refactor(editor): reorg code structure of store package (#9525)

This commit is contained in:
Saul-Mirone
2025-01-05 12:49:02 +00:00
parent 1180e9bc15
commit 3d168ba2d2
55 changed files with 618 additions and 635 deletions

View File

@@ -13,7 +13,6 @@ import {
type BlockModel, type BlockModel,
type Blocks, type Blocks,
type BlockSnapshot, type BlockSnapshot,
BlockViewType,
type DraftModel, type DraftModel,
type Query, type Query,
Slice, Slice,
@@ -194,7 +193,7 @@ async function renderNoteContent(
}); });
const query: Query = { const query: Query = {
mode: 'strict', mode: 'strict',
match: ids.map(id => ({ id, viewType: BlockViewType.Display })), match: ids.map(id => ({ id, viewType: 'display' })),
}; };
const previewDoc = doc.doc.getBlocks({ query }); const previewDoc = doc.doc.getBlocks({ query });
const previewSpec = SpecProvider.getInstance().getSpec('page:preview'); const previewSpec = SpecProvider.getInstance().getSpec('page:preview');

View File

@@ -25,12 +25,7 @@ import {
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { assertExists, Bound, getCommonBound } from '@blocksuite/global/utils'; import { assertExists, Bound, getCommonBound } from '@blocksuite/global/utils';
import { import { type GetBlocksOptions, type Query, Text } from '@blocksuite/store';
BlockViewType,
type GetBlocksOptions,
type Query,
Text,
} from '@blocksuite/store';
import { computed } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { html, type PropertyValues } from 'lit'; import { html, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js'; import { query, state } from 'lit/decorators.js';
@@ -106,7 +101,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
props: { props: {
displayMode: NoteDisplayMode.EdgelessOnly, displayMode: NoteDisplayMode.EdgelessOnly,
}, },
viewType: BlockViewType.Hidden, viewType: 'hidden',
}, },
], ],
}; };

View File

@@ -18,7 +18,7 @@ import {
ShadowlessElement, ShadowlessElement,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { deserializeXYWH, WithDisposable } from '@blocksuite/global/utils'; import { deserializeXYWH, WithDisposable } from '@blocksuite/global/utils';
import { type BlockModel, BlockViewType, type Query } from '@blocksuite/store'; import { type BlockModel, type Query } from '@blocksuite/store';
import { css, nothing } from 'lit'; import { css, nothing } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
@@ -48,7 +48,7 @@ export class SurfaceRefNotePortal extends WithDisposable(ShadowlessElement) {
mode: 'include', mode: 'include',
match: Array.from(ancestors).map(id => ({ match: Array.from(ancestors).map(id => ({
id, id,
viewType: BlockViewType.Display, viewType: 'display',
})), })),
}; };
this.query = query; this.query = query;

View File

@@ -5,7 +5,7 @@ import {
type DndEventState, type DndEventState,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { Point } from '@blocksuite/global/utils'; import { Point } from '@blocksuite/global/utils';
import { BlockViewType, type Query } from '@blocksuite/store'; import type { BlockViewType, Query } from '@blocksuite/store';
import { DragPreview } from '../components/drag-preview.js'; import { DragPreview } from '../components/drag-preview.js';
import type { AffineDragHandleWidget } from '../drag-handle.js'; import type { AffineDragHandleWidget } from '../drag-handle.js';
@@ -24,7 +24,7 @@ export class PreviewHelper {
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map( const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
id => ({ id => ({
id, id,
viewType: BlockViewType.Display, viewType: 'display',
}) })
); );
@@ -33,7 +33,7 @@ export class PreviewHelper {
let parent: string | null = block; let parent: string | null = block;
do { do {
if (!selectedIds.includes(parent)) { if (!selectedIds.includes(parent)) {
ids.push({ viewType: BlockViewType.Bypass, id: parent }); ids.push({ viewType: 'bypass', id: parent });
} }
parent = this.widget.doc.getParent(parent)?.id ?? null; parent = this.widget.doc.getParent(parent)?.id ?? null;
} while (parent && !ids.map(({ id }) => id).includes(parent)); } while (parent && !ids.map(({ id }) => id).includes(parent));
@@ -43,7 +43,7 @@ export class PreviewHelper {
const addChildren = (id: string) => { const addChildren = (id: string) => {
const children = this.widget.doc.getBlock(id)?.model.children ?? []; const children = this.widget.doc.getBlock(id)?.model.children ?? [];
children.forEach(child => { children.forEach(child => {
ids.push({ viewType: BlockViewType.Display, id: child.id }); ids.push({ viewType: 'display', id: child.id });
addChildren(child.id); addChildren(child.id);
}); });
}; };

View File

@@ -12,7 +12,7 @@ import {
DisposableGroup, DisposableGroup,
WithDisposable, WithDisposable,
} from '@blocksuite/global/utils'; } from '@blocksuite/global/utils';
import { type Blocks, BlockViewType, type Query } from '@blocksuite/store'; import { type Blocks, type Query } from '@blocksuite/store';
import { css, html, nothing, type PropertyValues } from 'lit'; import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
@@ -71,7 +71,7 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
match: [ match: [
{ {
flavour: 'affine:frame', flavour: 'affine:frame',
viewType: BlockViewType.Hidden, viewType: 'hidden',
}, },
], ],
}; };

View File

@@ -1,6 +1,6 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { type BlockModel, Blocks, BlockViewType } from '@blocksuite/store'; import { type BlockModel, Blocks, type BlockViewType } from '@blocksuite/store';
import { consume, provide } from '@lit/context'; import { consume, provide } from '@lit/context';
import { computed } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { nothing, type TemplateResult } from 'lit'; import { nothing, type TemplateResult } from 'lit';
@@ -187,9 +187,9 @@ export class BlockComponent<
private _renderViewType(content: unknown) { private _renderViewType(content: unknown) {
return choose(this.viewType, [ return choose(this.viewType, [
[BlockViewType.Display, () => content], ['display', () => content],
[BlockViewType.Hidden, () => nothing], ['hidden', () => nothing],
[BlockViewType.Bypass, () => this.renderChildren(this.model)], ['bypass', () => this.renderChildren(this.model)],
]); ]);
} }
@@ -310,7 +310,7 @@ export class BlockComponent<
accessor doc!: Blocks; accessor doc!: Blocks;
@property({ attribute: false }) @property({ attribute: false })
accessor viewType: BlockViewType = BlockViewType.Display; accessor viewType: BlockViewType = 'display';
@property({ @property({
attribute: false, attribute: false,

View File

@@ -4,7 +4,7 @@ import {
handleError, handleError,
} from '@blocksuite/global/exceptions'; } from '@blocksuite/global/exceptions';
import { SignalWatcher, Slot, WithDisposable } from '@blocksuite/global/utils'; import { SignalWatcher, Slot, WithDisposable } from '@blocksuite/global/utils';
import { type BlockModel, Blocks, BlockViewType } from '@blocksuite/store'; import { type BlockModel, Blocks } from '@blocksuite/store';
import { createContext, provide } from '@lit/context'; import { createContext, provide } from '@lit/context';
import { css, LitElement, nothing, type TemplateResult } from 'lit'; import { css, LitElement, nothing, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
@@ -44,7 +44,7 @@ export class EditorHost extends SignalWatcher(
private readonly _renderModel = (model: BlockModel): TemplateResult => { private readonly _renderModel = (model: BlockModel): TemplateResult => {
const { flavour } = model; const { flavour } = model;
const block = this.doc.getBlock(model.id); const block = this.doc.getBlock(model.id);
if (!block || block.blockViewType === BlockViewType.Hidden) { if (!block || block.blockViewType === 'hidden') {
return html`${nothing}`; return html`${nothing}`;
} }
const schema = this.doc.schema.flavourSchemaMap.get(flavour); const schema = this.doc.schema.flavourSchemaMap.get(flavour);

View File

@@ -3,14 +3,15 @@ import { describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { import {
Block,
defineBlockSchema, defineBlockSchema,
internalPrimitives, internalPrimitives,
Schema,
type SchemaToModel, type SchemaToModel,
} from '../schema/index.js'; } from '../model/block/index.js';
import { Block, type YBlock } from '../store/doc/block/index.js'; import type { YBlock } from '../model/block/types.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js'; import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
const pageSchema = defineBlockSchema({ const pageSchema = defineBlockSchema({
flavour: 'page', flavour: 'page',

View File

@@ -4,13 +4,11 @@ import type { Slot } from '@blocksuite/global/utils';
import { assert, beforeEach, describe, expect, it, vi } from 'vitest'; import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
import { applyUpdate, type Doc, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, type Doc, encodeStateAsUpdate } from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js'; import type { BlockModel, Blocks, BlockSchemaType, DocMeta } from '../index.js';
import type { BlockModel, Blocks, BlockSchemaType } from '../index.js';
import { Schema } from '../index.js'; import { Schema } from '../index.js';
import { Text } from '../reactive/text.js'; import { Text } from '../reactive/text.js';
import type { DocMeta } from '../store/workspace.js'; import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js'; import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
import { import {
NoteBlockSchema, NoteBlockSchema,
ParagraphBlockSchema, ParagraphBlockSchema,
@@ -115,8 +113,8 @@ describe('basic', () => {
tags: [], tags: [],
}, },
], ],
workspaceVersion: COLLECTION_VERSION, workspaceVersion: 2,
pageVersion: PAGE_VERSION, pageVersion: 2,
blockVersions: { blockVersions: {
'affine:note': 1, 'affine:note': 1,
'affine:page': 2, 'affine:page': 2,

View File

@@ -2,9 +2,8 @@ import { expect, test, vi } from 'vitest';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Schema } from '../schema/index.js'; import { Schema } from '../schema/index.js';
import { BlockViewType } from '../store/index.js'; import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js'; import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
import { import {
DividerBlockSchema, DividerBlockSchema,
ListBlockSchema, ListBlockSchema,
@@ -220,7 +219,7 @@ test('query', () => {
match: [ match: [
{ {
flavour: 'affine:list', flavour: 'affine:list',
viewType: BlockViewType.Hidden, viewType: 'hidden',
}, },
], ],
}, },
@@ -233,14 +232,14 @@ test('query', () => {
const paragraph1 = doc1.addBlock('affine:paragraph', {}, note); const paragraph1 = doc1.addBlock('affine:paragraph', {}, note);
const list1 = doc1.addBlock('affine:list' as never, {}, note); const list1 = doc1.addBlock('affine:list' as never, {}, note);
expect(doc2?.getBlock(paragraph1)?.blockViewType).toBe(BlockViewType.Display); expect(doc2?.getBlock(paragraph1)?.blockViewType).toBe('display');
expect(doc2?.getBlock(list1)?.blockViewType).toBe(BlockViewType.Display); expect(doc2?.getBlock(list1)?.blockViewType).toBe('display');
expect(doc3?.getBlock(list1)?.blockViewType).toBe(BlockViewType.Hidden); expect(doc3?.getBlock(list1)?.blockViewType).toBe('hidden');
const list2 = doc1.addBlock('affine:list' as never, {}, note); const list2 = doc1.addBlock('affine:list' as never, {}, note);
expect(doc2?.getBlock(list2)?.blockViewType).toBe(BlockViewType.Display); expect(doc2?.getBlock(list2)?.blockViewType).toBe('display');
expect(doc3?.getBlock(list2)?.blockViewType).toBe(BlockViewType.Hidden); expect(doc3?.getBlock(list2)?.blockViewType).toBe('hidden');
}); });
test('local readonly', () => { test('local readonly', () => {

View File

@@ -1,12 +1,13 @@
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import type { BlockModel } from '../model/block/block-model.js';
import { defineBlockSchema } from '../model/block/zod.js';
// import some blocks // import some blocks
import { type BlockModel, defineBlockSchema } from '../schema/base.js';
import { SchemaValidateError } from '../schema/error.js'; import { SchemaValidateError } from '../schema/error.js';
import { Schema } from '../schema/index.js'; import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js'; import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
import { import {
DividerBlockSchema, DividerBlockSchema,
ListBlockSchema, ListBlockSchema,

View File

@@ -1,4 +1,4 @@
import { defineBlockSchema, type SchemaToModel } from '../schema/index.js'; import { defineBlockSchema, type SchemaToModel } from '../model/index.js';
export const RootBlockSchema = defineBlockSchema({ export const RootBlockSchema = defineBlockSchema({
flavour: 'affine:page', flavour: 'affine:page',

View File

@@ -2,16 +2,13 @@ import { expect, test } from 'vitest';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { MemoryBlobCRUD } from '../adapter/index.js'; import { MemoryBlobCRUD } from '../adapter/index.js';
import type { BlockModel } from '../model/block/block-model.js';
import { defineBlockSchema, type SchemaToModel } from '../model/block/zod.js';
import { Text } from '../reactive/index.js'; import { Text } from '../reactive/index.js';
import { import { Schema } from '../schema/index.js';
type BlockModel, import { createAutoIncrementIdGenerator } from '../test/index.js';
defineBlockSchema,
Schema,
type SchemaToModel,
} from '../schema/index.js';
import { TestWorkspace } from '../test/test-workspace.js'; import { TestWorkspace } from '../test/test-workspace.js';
import { AssetsManager, BaseBlockTransformer } from '../transformer/index.js'; import { AssetsManager, BaseBlockTransformer } from '../transformer/index.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
const docSchema = defineBlockSchema({ const docSchema = defineBlockSchema({
flavour: 'page', flavour: 'page',

View File

@@ -1,8 +1,8 @@
import { BlockSuiteError } from '@blocksuite/global/exceptions'; import { BlockSuiteError } from '@blocksuite/global/exceptions';
import type { Blocks } from '../store/index.js'; import type { Blocks, DraftModel } from '../model/index.js';
import type { AssetsManager } from '../transformer/assets.js'; import type { AssetsManager } from '../transformer/assets.js';
import type { DraftModel, Job, Slice } from '../transformer/index.js'; import type { Job, Slice } from '../transformer/index.js';
import type { import type {
BlockSnapshot, BlockSnapshot,
DocSnapshot, DocSnapshot,

View File

@@ -1,7 +1,3 @@
export const COLLECTION_VERSION = 2;
export const PAGE_VERSION = 2;
export const SCHEMA_NOT_FOUND_MESSAGE = export const SCHEMA_NOT_FOUND_MESSAGE =
'Schema not found. The block flavour may not be registered.'; 'Schema not found. The block flavour may not be registered.';

View File

@@ -2,12 +2,11 @@
/// <reference path="../shim.d.ts" /> /// <reference path="../shim.d.ts" />
export * from './adapter/index.js'; export * from './adapter/index.js';
export * from './model/index.js';
export * from './reactive/index.js'; export * from './reactive/index.js';
export * from './schema/index.js'; export * from './schema/index.js';
export * from './store/index.js';
export * from './transformer/index.js'; export * from './transformer/index.js';
export { type IdGenerator, nanoid, uuidv4 } from './utils/id-generator.js'; export { type IdGenerator, nanoid, uuidv4 } from './utils/id-generator.js';
export * as Utils from './utils/utils.js';
export * from './yjs/index.js'; export * from './yjs/index.js';
const env = const env =

View File

@@ -0,0 +1,152 @@
import { type Disposable, Slot } from '@blocksuite/global/utils';
import { computed, type Signal, signal } from '@preact/signals-core';
import type { Text } from '../../reactive/index.js';
import type { Blocks } from '../blocks/blocks.js';
import type { YBlock } from './types.js';
import type { RoleType } from './zod.js';
type SignaledProps<Props> = Props & {
[P in keyof Props & string as `${P}$`]: Signal<Props[P]>;
};
/**
* The MagicProps function is used to append the props to the class.
* For example:
*
* ```ts
* class MyBlock extends MagicProps()<{ foo: string }> {}
* const myBlock = new MyBlock();
* // You'll get type checking for the foo prop
* myBlock.foo = 'bar';
* ```
*/
function MagicProps(): {
new <Props>(): Props;
} {
return class {} as never;
}
const modelLabel = Symbol('model_label');
// @ts-expect-error allow magic props
export class BlockModel<
Props extends object = object,
PropsSignal extends object = SignaledProps<Props>,
> extends MagicProps()<PropsSignal> {
private readonly _children = signal<string[]>([]);
/**
* @deprecated use doc instead
*/
page!: Blocks;
private readonly _childModels = computed(() => {
const value: BlockModel[] = [];
this._children.value.forEach(id => {
const block = this.page.getBlock$(id);
if (block) {
value.push(block.model);
}
});
return value;
});
private readonly _onCreated: Disposable;
private readonly _onDeleted: Disposable;
childMap = computed(() =>
this._children.value.reduce((map, id, index) => {
map.set(id, index);
return map;
}, new Map<string, number>())
);
created = new Slot();
deleted = new Slot();
flavour!: string;
id!: string;
isEmpty = computed(() => {
return this._children.value.length === 0;
});
keys!: string[];
// This is used to avoid https://stackoverflow.com/questions/55886792/infer-typescript-generic-class-type
[modelLabel]: Props = 'type_info_label' as never;
pop!: (prop: keyof Props & string) => void;
propsUpdated = new Slot<{ key: string }>();
role!: RoleType;
stash!: (prop: keyof Props & string) => void;
// text is optional
text?: Text;
version!: number;
yBlock!: YBlock;
get children() {
return this._childModels.value;
}
get doc() {
return this.page;
}
set doc(doc: Blocks) {
this.page = doc;
}
get parent() {
return this.doc.getParent(this);
}
constructor() {
super();
this._onCreated = this.created.once(() => {
this._children.value = this.yBlock.get('sys:children').toArray();
this.yBlock.get('sys:children').observe(event => {
this._children.value = event.target.toArray();
});
this.yBlock.observe(event => {
if (event.keysChanged.has('sys:children')) {
this._children.value = this.yBlock.get('sys:children').toArray();
}
});
});
this._onDeleted = this.deleted.once(() => {
this._onCreated.dispose();
});
}
dispose() {
this.created.dispose();
this.deleted.dispose();
this.propsUpdated.dispose();
}
firstChild(): BlockModel | null {
return this.children[0] || null;
}
lastChild(): BlockModel | null {
if (!this.children.length) {
return this;
}
return this.children[this.children.length - 1].lastChild();
}
[Symbol.dispose]() {
this._onCreated.dispose();
this._onDeleted.dispose();
}
}

View File

@@ -1,15 +1,14 @@
import type { Schema } from '../../../schema/index.js'; import type { Schema } from '../../schema/index.js';
import { BlockViewType } from '../consts.js'; import type { Blocks } from '../blocks/blocks.js';
import type { Blocks } from '../doc.js';
import { SyncController } from './sync-controller.js'; import { SyncController } from './sync-controller.js';
import type { BlockOptions, YBlock } from './types.js'; import type { BlockOptions, YBlock } from './types.js';
export * from './types.js'; export type BlockViewType = 'bypass' | 'display' | 'hidden';
export class Block { export class Block {
private readonly _syncController: SyncController; private readonly _syncController: SyncController;
blockViewType: BlockViewType = BlockViewType.Display; blockViewType: BlockViewType = 'display';
get flavour() { get flavour() {
return this._syncController.flavour; return this._syncController.flavour;

View File

@@ -1,4 +1,4 @@
import type { BlockModel } from '../schema/base.js'; import type { BlockModel } from './block-model.js';
type PropsInDraft = 'version' | 'flavour' | 'role' | 'id' | 'keys' | 'text'; type PropsInDraft = 'version' | 'flavour' | 'role' | 'id' | 'keys' | 'text';

View File

@@ -0,0 +1,5 @@
export * from './block.js';
export * from './block-model.js';
export * from './draft.js';
export * from './types.js';
export * from './zod.js';

View File

@@ -9,11 +9,12 @@ import {
native2Y, native2Y,
type UnRecord, type UnRecord,
y2Native, y2Native,
} from '../../../reactive/index.js'; } from '../../reactive/index.js';
import { BlockModel, internalPrimitives } from '../../../schema/base.js'; import type { Schema } from '../../schema/schema.js';
import type { Schema } from '../../../schema/schema.js'; import type { Blocks } from '../blocks/blocks.js';
import type { Blocks } from '../doc.js'; import { BlockModel } from './block-model.js';
import type { YBlock } from './types.js'; import type { YBlock } from './types.js';
import { internalPrimitives } from './zod.js';
/** /**
* @internal * @internal

View File

@@ -1,5 +1,6 @@
import type * as Y from 'yjs'; import type * as Y from 'yjs';
import type { BlockModel } from './block-model.js';
import type { Block } from './index.js'; import type { Block } from './index.js';
export type YBlock = Y.Map<unknown> & { export type YBlock = Y.Map<unknown> & {
@@ -11,3 +12,10 @@ export type YBlock = Y.Map<unknown> & {
export type BlockOptions = { export type BlockOptions = {
onChange?: (block: Block, key: string, value: unknown) => void; onChange?: (block: Block, key: string, value: unknown) => void;
}; };
export type BlockSysProps = {
id: string;
flavour: string;
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;

View File

@@ -0,0 +1,124 @@
import type * as Y from 'yjs';
import { z } from 'zod';
import { Boxed, Text } from '../../reactive/index.js';
import type { BaseBlockTransformer } from '../../transformer/base.js';
import type { BlockModel } from './block-model.js';
const FlavourSchema = z.string();
const ParentSchema = z.array(z.string()).optional();
const ContentSchema = z.array(z.string()).optional();
const role = ['root', 'hub', 'content'] as const;
const RoleSchema = z.enum(role);
export type RoleType = (typeof role)[number];
export interface InternalPrimitives {
Text: (input?: Y.Text | string) => Text;
Boxed: <T>(input: T) => Boxed<T>;
}
export const internalPrimitives: InternalPrimitives = Object.freeze({
Text: (input: Y.Text | string = '') => new Text(input),
Boxed: <T>(input: T) => new Boxed(input),
});
export const BlockSchema = z.object({
version: z.number(),
model: z.object({
role: RoleSchema,
flavour: FlavourSchema,
parent: ParentSchema,
children: ContentSchema,
props: z
.function()
.args(z.custom<InternalPrimitives>())
.returns(z.record(z.any()))
.optional(),
toModel: z.function().args().returns(z.custom<BlockModel>()).optional(),
}),
transformer: z
.function()
.args()
.returns(z.custom<BaseBlockTransformer>())
.optional(),
});
export type BlockSchemaType = z.infer<typeof BlockSchema>;
export type PropsGetter<Props> = (
internalPrimitives: InternalPrimitives
) => Props;
export type SchemaToModel<
Schema extends {
model: {
props: PropsGetter<object>;
flavour: string;
};
},
> = BlockModel<ReturnType<Schema['model']['props']>> &
ReturnType<Schema['model']['props']> & {
flavour: Schema['model']['flavour'];
};
export function defineBlockSchema<
Flavour extends string,
Role extends RoleType,
Props extends object,
Metadata extends Readonly<{
version: number;
role: Role;
parent?: string[];
children?: string[];
}>,
Model extends BlockModel<Props>,
Transformer extends BaseBlockTransformer<Props>,
>(options: {
flavour: Flavour;
metadata: Metadata;
props?: (internalPrimitives: InternalPrimitives) => Props;
toModel?: () => Model;
transformer?: () => Transformer;
}): {
version: number;
model: {
props: PropsGetter<Props>;
flavour: Flavour;
} & Metadata;
transformer?: () => Transformer;
};
export function defineBlockSchema({
flavour,
props,
metadata,
toModel,
transformer,
}: {
flavour: string;
metadata: {
version: number;
role: RoleType;
parent?: string[];
children?: string[];
};
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
toModel?: () => BlockModel;
transformer?: () => BaseBlockTransformer;
}): BlockSchemaType {
const schema = {
version: metadata.version,
model: {
role: metadata.role,
parent: metadata.parent,
children: metadata.children,
flavour,
props,
toModel,
},
transformer,
} satisfies z.infer<typeof BlockSchema>;
BlockSchema.parse(schema);
return schema;
}

View File

@@ -2,14 +2,18 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { type Disposable, Slot } from '@blocksuite/global/utils'; import { type Disposable, Slot } from '@blocksuite/global/utils';
import { signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import type { BlockModel, Schema } from '../../schema/index.js'; import type { Schema } from '../../schema/index.js';
import type { DraftModel } from '../../transformer/index.js'; import {
import { syncBlockProps } from '../../utils/utils.js'; Block,
import type { BlockProps, Doc } from '../workspace.js'; type BlockModel,
import type { BlockOptions } from './block/index.js'; type BlockOptions,
import { Block } from './block/index.js'; type BlockProps,
type DraftModel,
} from '../block/index.js';
import type { Doc } from '../doc.js';
import { DocCRUD } from './crud.js'; import { DocCRUD } from './crud.js';
import { type Query, runQuery } from './query.js'; import { type Query, runQuery } from './query.js';
import { syncBlockProps } from './utils.js';
type DocOptions = { type DocOptions = {
schema: Schema; schema: Schema;

View File

@@ -2,12 +2,10 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { native2Y } from '../../reactive/index.js'; import { native2Y } from '../../reactive/index.js';
import { import type { Schema } from '../../schema/index.js';
type BlockModel, import type { BlockModel } from '../block/block-model.js';
internalPrimitives, import type { YBlock } from '../block/types.js';
type Schema, import { internalPrimitives } from '../block/zod.js';
} from '../../schema/index.js';
import type { YBlock } from './index.js';
export class DocCRUD { export class DocCRUD {
get root(): string | null { get root(): string | null {

View File

@@ -0,0 +1,2 @@
export * from './blocks.js';
export * from './query.js';

View File

@@ -1,8 +1,6 @@
import isMatch from 'lodash.ismatch'; import isMatch from 'lodash.ismatch';
import type { BlockModel } from '../../schema/index.js'; import type { Block, BlockModel, BlockViewType } from '../block/index.js';
import type { Block } from './block/index.js';
import { BlockViewType } from './consts.js';
export type QueryMatch = { export type QueryMatch = {
id?: string; id?: string;
@@ -27,7 +25,7 @@ export function runQuery(query: Query, block: Block) {
const blockViewType = getBlockViewType(query, block); const blockViewType = getBlockViewType(query, block);
block.blockViewType = blockViewType; block.blockViewType = blockViewType;
if (blockViewType !== BlockViewType.Hidden) { if (blockViewType !== 'hidden') {
const queryMode = query.mode; const queryMode = query.mode;
setAncestorsToDisplayIfHidden(queryMode, block); setAncestorsToDisplayIfHidden(queryMode, block);
} }
@@ -46,8 +44,8 @@ function getBlockViewType(query: Query, block: Block): BlockViewType {
}, },
{} as Record<string, unknown> {} as Record<string, unknown>
); );
let blockViewType = let blockViewType: BlockViewType =
queryMode === 'loose' ? BlockViewType.Display : BlockViewType.Hidden; queryMode === 'loose' ? 'display' : 'hidden';
query.match.some(queryObject => { query.match.some(queryObject => {
const { const {
@@ -76,9 +74,8 @@ function setAncestorsToDisplayIfHidden(mode: QueryMode, block: Block) {
let parent = doc.getParent(block.model); let parent = doc.getParent(block.model);
while (parent) { while (parent) {
const parentBlock = doc.getBlock(parent.id); const parentBlock = doc.getBlock(parent.id);
if (parentBlock && parentBlock.blockViewType === BlockViewType.Hidden) { if (parentBlock && parentBlock.blockViewType === 'hidden') {
parentBlock.blockViewType = parentBlock.blockViewType = mode === 'include' ? 'display' : 'bypass';
mode === 'include' ? BlockViewType.Display : BlockViewType.Bypass;
} }
parent = doc.getParent(parent); parent = doc.getParent(parent);
} }

View File

@@ -1,11 +1,11 @@
import type { z } from 'zod'; import type { z } from 'zod';
import { SYS_KEYS } from '../consts.js'; import { SYS_KEYS } from '../../consts.js';
import { native2Y } from '../reactive/index.js'; import { native2Y } from '../../reactive/index.js';
import type { BlockModel, BlockSchema } from '../schema/base.js'; import type { BlockModel } from '../block/block-model.js';
import { internalPrimitives } from '../schema/base.js'; import type { BlockProps, YBlock } from '../block/types.js';
import type { YBlock } from '../store/doc/block/index.js'; import type { BlockSchema } from '../block/zod.js';
import type { BlockProps } from '../store/workspace.js'; import { internalPrimitives } from '../block/zod.js';
export function syncBlockProps( export function syncBlockProps(
schema: z.infer<typeof BlockSchema>, schema: z.infer<typeof BlockSchema>,
@@ -35,13 +35,3 @@ export function syncBlockProps(
model[key] = native2Y(value); model[key] = native2Y(value);
}); });
} }
export const hash = (str: string) => {
return str
.split('')
.reduce(
(prevHash, currVal) =>
((prevHash << 5) - prevHash + currVal.charCodeAt(0)) | 0,
0
);
};

View File

@@ -0,0 +1,68 @@
import type { Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs';
import type { Schema } from '../schema/schema.js';
import type { AwarenessStore } from '../yjs/awareness.js';
import type { YBlock } from './block/types.js';
import type { Blocks } from './blocks/blocks.js';
import type { Query } from './blocks/query.js';
import type { Workspace } from './workspace.js';
import type { DocMeta } from './workspace-meta.js';
export type GetBlocksOptions = {
query?: Query;
readonly?: boolean;
};
export type CreateBlocksOptions = GetBlocksOptions & {
id?: string;
};
export type YBlocks = Y.Map<YBlock>;
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 history(): Y.UndoManager;
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;
getBlocks(options?: GetBlocksOptions): Blocks;
clearQuery(query: Query, readonly?: boolean): void;
get loaded(): boolean;
get readonly(): boolean;
get awarenessStore(): AwarenessStore;
get workspace(): Workspace;
get rootDoc(): Y.Doc;
get spaceDoc(): Y.Doc;
get yBlocks(): Y.Map<YBlock>;
}

View File

@@ -0,0 +1,19 @@
import type { BlockModel } from './block/block-model.js';
export * from './block/index.js';
export * from './blocks/index.js';
export * from './doc.js';
export * from './workspace.js';
export * from './workspace-meta.js';
declare global {
namespace BlockSuite {
interface BlockModels {}
type Flavour = string & keyof BlockModels;
type ModelProps<Model> = Partial<
Model extends BlockModel<infer U> ? U : never
>;
}
}

View File

@@ -0,0 +1,50 @@
import type { Slot } from '@blocksuite/global/utils';
import type { Workspace } from './workspace.js';
export type Tag = {
id: string;
value: string;
color: string;
};
export type DocsPropertiesMeta = {
tags?: {
options: Tag[];
};
};
export interface DocMeta {
id: string;
title: string;
tags: string[];
createDate: number;
updatedDate?: number;
favorite?: boolean;
}
export interface WorkspaceMeta {
get docMetas(): DocMeta[];
addDocMeta(props: DocMeta, index?: number): void;
getDocMeta(id: string): DocMeta | undefined;
setDocMeta(id: string, props: Partial<DocMeta>): void;
removeDocMeta(id: string): void;
get properties(): DocsPropertiesMeta;
setProperties(meta: DocsPropertiesMeta): void;
get avatar(): string | undefined;
setAvatar(avatar: string): void;
get name(): string | undefined;
setName(name: string): void;
hasVersion: boolean;
writeVersion(workspace: Workspace): void;
get docs(): unknown[] | undefined;
initialize(): void;
commonFieldsUpdated: Slot;
docMetaAdded: Slot<string>;
docMetaRemoved: Slot<string>;
docMetaUpdated: Slot;
}

View File

@@ -0,0 +1,35 @@
import type { Slot } from '@blocksuite/global/utils';
import type { BlobEngine, DocEngine } from '@blocksuite/sync';
import type * as Y from 'yjs';
import type { Schema } from '../schema/schema.js';
import type { IdGenerator } from '../utils/id-generator.js';
import type { AwarenessStore } from '../yjs/awareness.js';
import type { Blocks } from './blocks/blocks.js';
import type { CreateBlocksOptions, Doc, GetBlocksOptions } from './doc.js';
import type { WorkspaceMeta } from './workspace-meta.js';
export interface Workspace {
readonly id: string;
readonly meta: WorkspaceMeta;
readonly idGenerator: IdGenerator;
readonly docSync: DocEngine;
readonly blobSync: BlobEngine;
readonly awarenessStore: AwarenessStore;
get schema(): Schema;
get doc(): Y.Doc;
get docs(): Map<string, Doc>;
slots: {
docListUpdated: Slot;
docCreated: Slot<string>;
docRemoved: Slot<string>;
};
createDoc(options?: CreateBlocksOptions): Blocks;
getDoc(docId: string, options?: GetBlocksOptions): Blocks | null;
removeDoc(docId: string): void;
dispose(): void;
}

View File

@@ -1,274 +0,0 @@
import { type Disposable, Slot } from '@blocksuite/global/utils';
import type { Signal } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import type * as Y from 'yjs';
import { z } from 'zod';
import { Boxed } from '../reactive/boxed.js';
import { Text } from '../reactive/text.js';
import type { YBlock } from '../store/doc/block/index.js';
import type { Blocks } from '../store/index.js';
import type { BaseBlockTransformer } from '../transformer/base.js';
const FlavourSchema = z.string();
const ParentSchema = z.array(z.string()).optional();
const ContentSchema = z.array(z.string()).optional();
const role = ['root', 'hub', 'content'] as const;
const RoleSchema = z.enum(role);
export type RoleType = (typeof role)[number];
export interface InternalPrimitives {
Text: (input?: Y.Text | string) => Text;
Boxed: <T>(input: T) => Boxed<T>;
}
export const internalPrimitives: InternalPrimitives = Object.freeze({
Text: (input: Y.Text | string = '') => new Text(input),
Boxed: <T>(input: T) => new Boxed(input),
});
export const BlockSchema = z.object({
version: z.number(),
model: z.object({
role: RoleSchema,
flavour: FlavourSchema,
parent: ParentSchema,
children: ContentSchema,
props: z
.function()
.args(z.custom<InternalPrimitives>())
.returns(z.record(z.any()))
.optional(),
toModel: z.function().args().returns(z.custom<BlockModel>()).optional(),
}),
transformer: z
.function()
.args()
.returns(z.custom<BaseBlockTransformer>())
.optional(),
});
export type BlockSchemaType = z.infer<typeof BlockSchema>;
export type PropsGetter<Props> = (
internalPrimitives: InternalPrimitives
) => Props;
export type SchemaToModel<
Schema extends {
model: {
props: PropsGetter<object>;
flavour: string;
};
},
> = BlockModel<ReturnType<Schema['model']['props']>> &
ReturnType<Schema['model']['props']> & {
flavour: Schema['model']['flavour'];
};
export function defineBlockSchema<
Flavour extends string,
Role extends RoleType,
Props extends object,
Metadata extends Readonly<{
version: number;
role: Role;
parent?: string[];
children?: string[];
}>,
Model extends BlockModel<Props>,
Transformer extends BaseBlockTransformer<Props>,
>(options: {
flavour: Flavour;
metadata: Metadata;
props?: (internalPrimitives: InternalPrimitives) => Props;
toModel?: () => Model;
transformer?: () => Transformer;
}): {
version: number;
model: {
props: PropsGetter<Props>;
flavour: Flavour;
} & Metadata;
transformer?: () => Transformer;
};
export function defineBlockSchema({
flavour,
props,
metadata,
toModel,
transformer,
}: {
flavour: string;
metadata: {
version: number;
role: RoleType;
parent?: string[];
children?: string[];
};
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
toModel?: () => BlockModel;
transformer?: () => BaseBlockTransformer;
}): BlockSchemaType {
const schema = {
version: metadata.version,
model: {
role: metadata.role,
parent: metadata.parent,
children: metadata.children,
flavour,
props,
toModel,
},
transformer,
} satisfies z.infer<typeof BlockSchema>;
BlockSchema.parse(schema);
return schema;
}
type SignaledProps<Props> = Props & {
[P in keyof Props & string as `${P}$`]: Signal<Props[P]>;
};
/**
* The MagicProps function is used to append the props to the class.
* For example:
*
* ```ts
* class MyBlock extends MagicProps()<{ foo: string }> {}
* const myBlock = new MyBlock();
* // You'll get type checking for the foo prop
* myBlock.foo = 'bar';
* ```
*/
function MagicProps(): {
new <Props>(): Props;
} {
return class {} as never;
}
const modelLabel = Symbol('model_label');
// @ts-expect-error allow magic props
export class BlockModel<
Props extends object = object,
PropsSignal extends object = SignaledProps<Props>,
> extends MagicProps()<PropsSignal> {
private readonly _children = signal<string[]>([]);
/**
* @deprecated use doc instead
*/
page!: Blocks;
private readonly _childModels = computed(() => {
const value: BlockModel[] = [];
this._children.value.forEach(id => {
const block = this.page.getBlock$(id);
if (block) {
value.push(block.model);
}
});
return value;
});
private readonly _onCreated: Disposable;
private readonly _onDeleted: Disposable;
childMap = computed(() =>
this._children.value.reduce((map, id, index) => {
map.set(id, index);
return map;
}, new Map<string, number>())
);
created = new Slot();
deleted = new Slot();
flavour!: string;
id!: string;
isEmpty = computed(() => {
return this._children.value.length === 0;
});
keys!: string[];
// This is used to avoid https://stackoverflow.com/questions/55886792/infer-typescript-generic-class-type
[modelLabel]: Props = 'type_info_label' as never;
pop!: (prop: keyof Props & string) => void;
propsUpdated = new Slot<{ key: string }>();
role!: RoleType;
stash!: (prop: keyof Props & string) => void;
// text is optional
text?: Text;
version!: number;
yBlock!: YBlock;
get children() {
return this._childModels.value;
}
get doc() {
return this.page;
}
set doc(doc: Blocks) {
this.page = doc;
}
get parent() {
return this.doc.getParent(this);
}
constructor() {
super();
this._onCreated = this.created.once(() => {
this._children.value = this.yBlock.get('sys:children').toArray();
this.yBlock.get('sys:children').observe(event => {
this._children.value = event.target.toArray();
});
this.yBlock.observe(event => {
if (event.keysChanged.has('sys:children')) {
this._children.value = this.yBlock.get('sys:children').toArray();
}
});
});
this._onDeleted = this.deleted.once(() => {
this._onCreated.dispose();
});
}
dispose() {
this.created.dispose();
this.deleted.dispose();
this.propsUpdated.dispose();
}
firstChild(): BlockModel | null {
return this.children[0] || null;
}
lastChild(): BlockModel | null {
if (!this.children.length) {
return this;
}
return this.children[this.children.length - 1].lastChild();
}
[Symbol.dispose]() {
this._onCreated.dispose();
this._onDeleted.dispose();
}
}

View File

@@ -1,15 +1,5 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
export class MigrationError extends BlockSuiteError {
constructor(description: string) {
super(
ErrorCode.MigrationError,
`Migration failed. Please report to https://github.com/toeverything/blocksuite/issues
${description}`
);
}
}
export class SchemaValidateError extends BlockSuiteError { export class SchemaValidateError extends BlockSuiteError {
constructor(flavour: string, message: string) { constructor(flavour: string, message: string) {
super( super(

View File

@@ -1,2 +1 @@
export * from './base.js';
export { Schema } from './schema.js'; export { Schema } from './schema.js';

View File

@@ -1,8 +1,7 @@
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js'; import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
import type { BlockSchemaType } from './base.js'; import { BlockSchema, type BlockSchemaType } from '../model/index.js';
import { BlockSchema } from './base.js';
import { SchemaValidateError } from './error.js'; import { SchemaValidateError } from './error.js';
export class Schema { export class Schema {

View File

@@ -1,5 +0,0 @@
export enum BlockViewType {
Bypass = 'bypass',
Display = 'display',
Hidden = 'hidden',
}

View File

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

View File

@@ -1,2 +0,0 @@
export * from './doc/index.js';
export * from './workspace.js';

View File

@@ -1,166 +0,0 @@
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 { YBlock } from './doc/block/types.js';
import type { Blocks } from './doc/doc.js';
import type { Query } from './doc/query.js';
export type Tag = {
id: string;
value: string;
color: string;
};
export type DocsPropertiesMeta = {
tags?: {
options: Tag[];
};
};
export interface DocMeta {
id: string;
title: string;
tags: string[];
createDate: number;
updatedDate?: number;
favorite?: boolean;
}
export type GetBlocksOptions = {
query?: Query;
readonly?: boolean;
};
export type CreateBlocksOptions = GetBlocksOptions & {
id?: string;
};
export interface WorkspaceMeta {
get docMetas(): DocMeta[];
addDocMeta(props: DocMeta, index?: number): void;
getDocMeta(id: string): DocMeta | undefined;
setDocMeta(id: string, props: Partial<DocMeta>): void;
removeDocMeta(id: string): void;
get properties(): DocsPropertiesMeta;
setProperties(meta: DocsPropertiesMeta): void;
get avatar(): string | undefined;
setAvatar(avatar: string): void;
get name(): string | undefined;
setName(name: string): void;
hasVersion: boolean;
writeVersion(workspace: Workspace): void;
get docs(): unknown[] | undefined;
initialize(): void;
commonFieldsUpdated: Slot;
docMetaAdded: Slot<string>;
docMetaRemoved: Slot<string>;
docMetaUpdated: Slot;
}
export interface Workspace {
readonly id: string;
readonly meta: WorkspaceMeta;
readonly idGenerator: IdGenerator;
readonly docSync: DocEngine;
readonly blobSync: BlobEngine;
readonly awarenessStore: AwarenessStore;
get schema(): Schema;
get doc(): Y.Doc;
get docs(): Map<string, Doc>;
slots: {
docListUpdated: Slot;
docCreated: Slot<string>;
docRemoved: Slot<string>;
};
createDoc(options?: CreateBlocksOptions): Blocks;
getDoc(docId: string, options?: GetBlocksOptions): Blocks | null;
removeDoc(docId: string): void;
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 history(): Y.UndoManager;
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;
getBlocks(options?: GetBlocksOptions): Blocks;
clearQuery(query: Query, readonly?: boolean): void;
get loaded(): boolean;
get readonly(): boolean;
get awarenessStore(): AwarenessStore;
get workspace(): Workspace;
get rootDoc(): Y.Doc;
get spaceDoc(): Y.Doc;
get yBlocks(): Y.Map<YBlock>;
}
export interface StackItem {
meta: Map<'selection-state', unknown>;
}
export type YBlocks = Y.Map<YBlock>;
/** JSON-serializable properties of a block */
export type BlockSysProps = {
id: string;
flavour: string;
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
declare global {
namespace BlockSuite {
interface BlockModels {}
type Flavour = string & keyof BlockModels;
type ModelProps<Model> = Partial<
Model extends BlockModel<infer U> ? U : never
>;
}
}

View File

@@ -1,4 +1,10 @@
export { createAutoIncrementIdGenerator } from '../utils/id-generator.js'; import type { IdGenerator } from '../utils/id-generator.js';
export * from './test-doc.js'; export * from './test-doc.js';
export * from './test-meta.js'; export * from './test-meta.js';
export * from './test-workspace.js'; export * from './test-workspace.js';
export function createAutoIncrementIdGenerator(): IdGenerator {
let i = 0;
return () => (i++).toString();
}

View File

@@ -2,10 +2,10 @@ import { type Disposable, Slot } from '@blocksuite/global/utils';
import { signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Blocks } from '../store/doc/doc.js'; import type { YBlock } from '../model/block/types.js';
import type { YBlock } from '../store/doc/index.js'; import { Blocks } from '../model/blocks/blocks.js';
import type { Query } from '../store/doc/query.js'; import type { Query } from '../model/blocks/query.js';
import type { Doc, GetBlocksOptions, Workspace } from '../store/workspace.js'; import type { Doc, GetBlocksOptions, Workspace } from '../model/index.js';
import type { AwarenessStore } from '../yjs/index.js'; import type { AwarenessStore } from '../yjs/index.js';
type DocOptions = { type DocOptions = {

View File

@@ -1,16 +1,18 @@
import { Slot } from '@blocksuite/global/utils'; import { Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs'; import type * as Y from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js';
import { createYProxy } from '../reactive/proxy.js';
import type { import type {
DocMeta, DocMeta,
DocsPropertiesMeta, DocsPropertiesMeta,
Workspace, Workspace,
WorkspaceMeta, WorkspaceMeta,
} from '../store/workspace.js'; } from '../model/index.js';
import { createYProxy } from '../reactive/proxy.js';
export type DocCollectionMetaState = { const COLLECTION_VERSION = 2;
const PAGE_VERSION = 2;
type DocCollectionMetaState = {
pages?: unknown[]; pages?: unknown[];
properties?: DocsPropertiesMeta; properties?: DocsPropertiesMeta;
workspaceVersion?: number; workspaceVersion?: number;

View File

@@ -16,14 +16,14 @@ import merge from 'lodash.merge';
import { Awareness } from 'y-protocols/awareness.js'; import { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
import type {
Blocks,
CreateBlocksOptions,
GetBlocksOptions,
Workspace,
WorkspaceMeta,
} from '../model/index.js';
import type { Schema } from '../schema/index.js'; import type { Schema } from '../schema/index.js';
import {
type Blocks,
type CreateBlocksOptions,
type GetBlocksOptions,
type Workspace,
type WorkspaceMeta,
} from '../store/index.js';
import { type IdGenerator, nanoid } from '../utils/id-generator.js'; import { type IdGenerator, nanoid } from '../utils/id-generator.js';
import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js'; import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js';
import { TestDoc } from './test-doc.js'; import { TestDoc } from './test-doc.js';

View File

@@ -1,7 +1,10 @@
import type { BlockModel, InternalPrimitives } from '../schema/index.js'; import type { BlockModel } from '../model/block/block-model.js';
import { internalPrimitives } from '../schema/index.js'; import type { DraftModel } from '../model/block/draft.js';
import {
type InternalPrimitives,
internalPrimitives,
} from '../model/block/zod.js';
import type { AssetsManager } from './assets.js'; import type { AssetsManager } from './assets.js';
import type { DraftModel } from './draft.js';
import { fromJSON, toJSON } from './json.js'; import { fromJSON, toJSON } from './json.js';
import type { BlockSnapshot } from './type.js'; import type { BlockSnapshot } from './type.js';

View File

@@ -1,6 +1,5 @@
export * from './assets.js'; export * from './assets.js';
export * from './base.js'; export * from './base.js';
export * from './draft.js';
export * from './job.js'; export * from './job.js';
export * from './json.js'; export * from './json.js';
export * from './middleware.js'; export * from './middleware.js';

View File

@@ -1,11 +1,15 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { nextTick, Slot } from '@blocksuite/global/utils'; import { nextTick, Slot } from '@blocksuite/global/utils';
import type { BlockModel, BlockSchemaType, Schema } from '../schema/index.js'; import type {
import type { Blocks } from '../store/index.js'; BlockModel,
Blocks,
BlockSchemaType,
DraftModel,
} from '../model/index.js';
import type { Schema } from '../schema/index.js';
import { AssetsManager } from './assets.js'; import { AssetsManager } from './assets.js';
import { BaseBlockTransformer } from './base.js'; import { BaseBlockTransformer } from './base.js';
import type { DraftModel } from './draft.js';
import type { import type {
BeforeExportPayload, BeforeExportPayload,
BeforeImportPayload, BeforeImportPayload,

View File

@@ -1,8 +1,7 @@
import type { Slot } from '@blocksuite/global/utils'; import type { Slot } from '@blocksuite/global/utils';
import type { Blocks } from '../store/index.js'; import type { Blocks, DraftModel } from '../model/index.js';
import type { AssetsManager } from './assets.js'; import type { AssetsManager } from './assets.js';
import type { DraftModel } from './draft.js';
import type { Slice } from './slice.js'; import type { Slice } from './slice.js';
import type { import type {
BlockSnapshot, BlockSnapshot,

View File

@@ -1,5 +1,4 @@
import type { Blocks } from '../store/index.js'; import type { Blocks, DraftModel } from '../model/index.js';
import type { DraftModel } from './draft.js';
type SliceData = { type SliceData = {
content: DraftModel[]; content: DraftModel[];

View File

@@ -1,7 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import type { Blocks } from '../store/doc/doc.js'; import type { Blocks } from '../model/blocks/blocks.js';
import type { DocMeta, DocsPropertiesMeta } from '../store/workspace.js'; import type { DocMeta, DocsPropertiesMeta } from '../model/workspace-meta.js';
export type BlockSnapshot = { export type BlockSnapshot = {
type: 'block'; type: 'block';

View File

@@ -3,11 +3,6 @@ import { nanoid as nanoidGenerator } from 'nanoid';
export type IdGenerator = () => string; export type IdGenerator = () => string;
export function createAutoIncrementIdGenerator(): IdGenerator {
let i = 0;
return () => (i++).toString();
}
export const uuidv4: IdGenerator = () => { export const uuidv4: IdGenerator = () => {
return uuidv4IdGenerator(); return uuidv4IdGenerator();
}; };

View File

@@ -5,7 +5,7 @@ import clonedeep from 'lodash.clonedeep';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import type { Awareness as YAwareness } from 'y-protocols/awareness.js'; import type { Awareness as YAwareness } from 'y-protocols/awareness.js';
import type { Doc } from '../store/index.js'; import type { Doc } from '../model/doc.js';
export interface UserInfo { export interface UserInfo {
name: string; name: string;
@@ -14,25 +14,22 @@ export interface UserInfo {
type UserSelection = Array<Record<string, unknown>>; type UserSelection = Array<Record<string, unknown>>;
// Raw JSON state in awareness CRDT // Raw JSON state in awareness CRDT
export type RawAwarenessState<Flags extends BlockSuiteFlags = BlockSuiteFlags> = export type RawAwarenessState = {
{ user?: UserInfo;
user?: UserInfo; color?: string;
color?: string; flags: BlockSuiteFlags;
flags: Flags; // use v2 to avoid crush on old clients
// use v2 to avoid crush on old clients selectionV2: Record<string, UserSelection>;
selectionV2: Record<string, UserSelection>; };
};
export interface AwarenessEvent< export interface AwarenessEvent {
Flags extends BlockSuiteFlags = BlockSuiteFlags,
> {
id: number; id: number;
type: 'add' | 'update' | 'remove'; type: 'add' | 'update' | 'remove';
state?: RawAwarenessState<Flags>; state?: RawAwarenessState;
} }
export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> { export class AwarenessStore {
private readonly _flags: Signal<Flags>; private readonly _flags: Signal<BlockSuiteFlags>;
private readonly _onAwarenessChange = (diff: { private readonly _onAwarenessChange = (diff: {
added: number[]; added: number[];
@@ -66,24 +63,24 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
}); });
}; };
readonly awareness: YAwareness<RawAwarenessState<Flags>>; readonly awareness: YAwareness<RawAwarenessState>;
readonly slots = { readonly slots = {
update: new Slot<AwarenessEvent<Flags>>(), update: new Slot<AwarenessEvent>(),
}; };
constructor( constructor(
awareness: YAwareness<RawAwarenessState<Flags>>, awareness: YAwareness<RawAwarenessState>,
defaultFlags: Flags defaultFlags: BlockSuiteFlags
) { ) {
this._flags = signal<Flags>(defaultFlags); this._flags = signal(defaultFlags);
this.awareness = awareness; this.awareness = awareness;
this.awareness.on('change', this._onAwarenessChange); this.awareness.on('change', this._onAwarenessChange);
this.awareness.setLocalStateField('selectionV2', {}); this.awareness.setLocalStateField('selectionV2', {});
this._initFlags(defaultFlags); this._initFlags(defaultFlags);
} }
private _initFlags(defaultFlags: Flags) { private _initFlags(defaultFlags: BlockSuiteFlags) {
const upstreamFlags = this.awareness.getLocalState()?.flags; const upstreamFlags = this.awareness.getLocalState()?.flags;
const flags = clonedeep(defaultFlags); const flags = clonedeep(defaultFlags);
if (upstreamFlags) { if (upstreamFlags) {
@@ -98,7 +95,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
this.awareness.destroy(); this.awareness.destroy();
} }
getFlag<Key extends keyof Flags>(field: Key) { getFlag<Key extends keyof BlockSuiteFlags>(field: Key) {
return this._flags.value[field]; return this._flags.value[field];
} }
@@ -111,7 +108,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
); );
} }
getStates(): Map<number, RawAwarenessState<Flags>> { getStates(): Map<number, RawAwarenessState> {
return this.awareness.getStates(); return this.awareness.getStates();
} }
@@ -124,7 +121,10 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
} }
} }
setFlag<Key extends keyof Flags>(field: Key, value: Flags[Key]) { setFlag<Key extends keyof BlockSuiteFlags>(
field: Key,
value: BlockSuiteFlags[Key]
) {
const oldFlags = this.awareness.getLocalState()?.flags ?? {}; const oldFlags = this.awareness.getLocalState()?.flags ?? {};
this.awareness.setLocalStateField('flags', { ...oldFlags, [field]: value }); this.awareness.setLocalStateField('flags', { ...oldFlags, [field]: value });
} }
@@ -142,6 +142,6 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
this.setFlag('readonly', { this.setFlag('readonly', {
...flags, ...flags,
[blockCollection.id]: value, [blockCollection.id]: value,
} as Flags['readonly']); } as BlockSuiteFlags['readonly']);
} }
} }

View File

@@ -5,3 +5,7 @@ export type SubdocEvent = {
removed: Set<YDoc>; removed: Set<YDoc>;
added: Set<YDoc>; added: Set<YDoc>;
}; };
export interface StackItem {
meta: Map<'selection-state', unknown>;
}

View File

@@ -16,7 +16,6 @@ import { BLOCK_ID_ATTR } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { InlineRootElement } from '@inline/inline-editor.js'; import type { InlineRootElement } from '@inline/inline-editor.js';
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { COLLECTION_VERSION, PAGE_VERSION } from '@store/consts.js';
import type { BlockModel } from '@store/index.js'; import type { BlockModel } from '@store/index.js';
import { import {
@@ -88,8 +87,8 @@ export const defaultStore = {
'affine:surface-ref': 1, 'affine:surface-ref': 1,
'affine:edgeless-text': 1, 'affine:edgeless-text': 1,
}, },
workspaceVersion: COLLECTION_VERSION, workspaceVersion: 2,
pageVersion: PAGE_VERSION, pageVersion: 2,
}, },
spaces: { spaces: {
'doc:home': { 'doc:home': {

View File

@@ -21,7 +21,6 @@ import { Container, type ServiceProvider } from '@blocksuite/affine/global/di';
import { WithDisposable } from '@blocksuite/affine/global/utils'; import { WithDisposable } from '@blocksuite/affine/global/utils';
import { import {
type Blocks, type Blocks,
BlockViewType,
type JobMiddleware, type JobMiddleware,
type Query, type Query,
type Schema, type Schema,
@@ -186,7 +185,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
'affine:code', 'affine:code',
'affine:list', 'affine:list',
'affine:divider', 'affine:divider',
].map(flavour => ({ flavour, viewType: BlockViewType.Display })), ].map(flavour => ({ flavour, viewType: 'display' })),
}; };
private _timer?: ReturnType<typeof setInterval> | null = null; private _timer?: ReturnType<typeof setInterval> | null = null;