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

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

View File

@@ -4,7 +4,7 @@ import {
handleError,
} from '@blocksuite/global/exceptions';
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 { css, LitElement, nothing, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
@@ -44,7 +44,7 @@ export class EditorHost extends SignalWatcher(
private readonly _renderModel = (model: BlockModel): TemplateResult => {
const { flavour } = model;
const block = this.doc.getBlock(model.id);
if (!block || block.blockViewType === BlockViewType.Hidden) {
if (!block || block.blockViewType === 'hidden') {
return html`${nothing}`;
}
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 {
Block,
defineBlockSchema,
internalPrimitives,
Schema,
type SchemaToModel,
} from '../schema/index.js';
import { Block, type YBlock } from '../store/doc/block/index.js';
} from '../model/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 { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
const pageSchema = defineBlockSchema({
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 { applyUpdate, type Doc, encodeStateAsUpdate } from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js';
import type { BlockModel, Blocks, BlockSchemaType } from '../index.js';
import type { BlockModel, Blocks, BlockSchemaType, DocMeta } from '../index.js';
import { Schema } from '../index.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 { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
import {
NoteBlockSchema,
ParagraphBlockSchema,
@@ -115,8 +113,8 @@ describe('basic', () => {
tags: [],
},
],
workspaceVersion: COLLECTION_VERSION,
pageVersion: PAGE_VERSION,
workspaceVersion: 2,
pageVersion: 2,
blockVersions: {
'affine:note': 1,
'affine:page': 2,

View File

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

View File

@@ -1,12 +1,13 @@
import { literal } from 'lit/static-html.js';
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 { type BlockModel, defineBlockSchema } from '../schema/base.js';
import { SchemaValidateError } from '../schema/error.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
import {
DividerBlockSchema,
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({
flavour: 'affine:page',

View File

@@ -2,16 +2,13 @@ import { expect, test } from 'vitest';
import * as Y from 'yjs';
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 {
type BlockModel,
defineBlockSchema,
Schema,
type SchemaToModel,
} from '../schema/index.js';
import { Schema } from '../schema/index.js';
import { createAutoIncrementIdGenerator } from '../test/index.js';
import { TestWorkspace } from '../test/test-workspace.js';
import { AssetsManager, BaseBlockTransformer } from '../transformer/index.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
const docSchema = defineBlockSchema({
flavour: 'page',

View File

@@ -1,8 +1,8 @@
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 { DraftModel, Job, Slice } from '../transformer/index.js';
import type { Job, Slice } from '../transformer/index.js';
import type {
BlockSnapshot,
DocSnapshot,

View File

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

View File

@@ -2,12 +2,11 @@
/// <reference path="../shim.d.ts" />
export * from './adapter/index.js';
export * from './model/index.js';
export * from './reactive/index.js';
export * from './schema/index.js';
export * from './store/index.js';
export * from './transformer/index.js';
export { type IdGenerator, nanoid, uuidv4 } from './utils/id-generator.js';
export * as Utils from './utils/utils.js';
export * from './yjs/index.js';
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 { BlockViewType } from '../consts.js';
import type { Blocks } from '../doc.js';
import type { Schema } from '../../schema/index.js';
import type { Blocks } from '../blocks/blocks.js';
import { SyncController } from './sync-controller.js';
import type { BlockOptions, YBlock } from './types.js';
export * from './types.js';
export type BlockViewType = 'bypass' | 'display' | 'hidden';
export class Block {
private readonly _syncController: SyncController;
blockViewType: BlockViewType = BlockViewType.Display;
blockViewType: BlockViewType = 'display';
get 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';

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,
type UnRecord,
y2Native,
} from '../../../reactive/index.js';
import { BlockModel, internalPrimitives } from '../../../schema/base.js';
import type { Schema } from '../../../schema/schema.js';
import type { Blocks } from '../doc.js';
} from '../../reactive/index.js';
import type { Schema } from '../../schema/schema.js';
import type { Blocks } from '../blocks/blocks.js';
import { BlockModel } from './block-model.js';
import type { YBlock } from './types.js';
import { internalPrimitives } from './zod.js';
/**
* @internal

View File

@@ -1,5 +1,6 @@
import type * as Y from 'yjs';
import type { BlockModel } from './block-model.js';
import type { Block } from './index.js';
export type YBlock = Y.Map<unknown> & {
@@ -11,3 +12,10 @@ export type YBlock = Y.Map<unknown> & {
export type BlockOptions = {
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 { signal } from '@preact/signals-core';
import type { BlockModel, Schema } from '../../schema/index.js';
import type { DraftModel } from '../../transformer/index.js';
import { syncBlockProps } from '../../utils/utils.js';
import type { BlockProps, Doc } from '../workspace.js';
import type { BlockOptions } from './block/index.js';
import { Block } from './block/index.js';
import type { Schema } from '../../schema/index.js';
import {
Block,
type BlockModel,
type BlockOptions,
type BlockProps,
type DraftModel,
} from '../block/index.js';
import type { Doc } from '../doc.js';
import { DocCRUD } from './crud.js';
import { type Query, runQuery } from './query.js';
import { syncBlockProps } from './utils.js';
type DocOptions = {
schema: Schema;

View File

@@ -2,12 +2,10 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import * as Y from 'yjs';
import { native2Y } from '../../reactive/index.js';
import {
type BlockModel,
internalPrimitives,
type Schema,
} from '../../schema/index.js';
import type { YBlock } from './index.js';
import type { Schema } from '../../schema/index.js';
import type { BlockModel } from '../block/block-model.js';
import type { YBlock } from '../block/types.js';
import { internalPrimitives } from '../block/zod.js';
export class DocCRUD {
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 type { BlockModel } from '../../schema/index.js';
import type { Block } from './block/index.js';
import { BlockViewType } from './consts.js';
import type { Block, BlockModel, BlockViewType } from '../block/index.js';
export type QueryMatch = {
id?: string;
@@ -27,7 +25,7 @@ export function runQuery(query: Query, block: Block) {
const blockViewType = getBlockViewType(query, block);
block.blockViewType = blockViewType;
if (blockViewType !== BlockViewType.Hidden) {
if (blockViewType !== 'hidden') {
const queryMode = query.mode;
setAncestorsToDisplayIfHidden(queryMode, block);
}
@@ -46,8 +44,8 @@ function getBlockViewType(query: Query, block: Block): BlockViewType {
},
{} as Record<string, unknown>
);
let blockViewType =
queryMode === 'loose' ? BlockViewType.Display : BlockViewType.Hidden;
let blockViewType: BlockViewType =
queryMode === 'loose' ? 'display' : 'hidden';
query.match.some(queryObject => {
const {
@@ -76,9 +74,8 @@ function setAncestorsToDisplayIfHidden(mode: QueryMode, block: Block) {
let parent = doc.getParent(block.model);
while (parent) {
const parentBlock = doc.getBlock(parent.id);
if (parentBlock && parentBlock.blockViewType === BlockViewType.Hidden) {
parentBlock.blockViewType =
mode === 'include' ? BlockViewType.Display : BlockViewType.Bypass;
if (parentBlock && parentBlock.blockViewType === 'hidden') {
parentBlock.blockViewType = mode === 'include' ? 'display' : 'bypass';
}
parent = doc.getParent(parent);
}

View File

@@ -1,11 +1,11 @@
import type { z } from 'zod';
import { SYS_KEYS } from '../consts.js';
import { native2Y } from '../reactive/index.js';
import type { BlockModel, BlockSchema } from '../schema/base.js';
import { internalPrimitives } from '../schema/base.js';
import type { YBlock } from '../store/doc/block/index.js';
import type { BlockProps } from '../store/workspace.js';
import { SYS_KEYS } from '../../consts.js';
import { native2Y } from '../../reactive/index.js';
import type { BlockModel } from '../block/block-model.js';
import type { BlockProps, YBlock } from '../block/types.js';
import type { BlockSchema } from '../block/zod.js';
import { internalPrimitives } from '../block/zod.js';
export function syncBlockProps(
schema: z.infer<typeof BlockSchema>,
@@ -35,13 +35,3 @@ export function syncBlockProps(
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';
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 {
constructor(flavour: string, message: string) {
super(

View File

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

View File

@@ -1,8 +1,7 @@
import { minimatch } from 'minimatch';
import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
import type { BlockSchemaType } from './base.js';
import { BlockSchema } from './base.js';
import { BlockSchema, type BlockSchemaType } from '../model/index.js';
import { SchemaValidateError } from './error.js';
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-meta.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 * as Y from 'yjs';
import { Blocks } from '../store/doc/doc.js';
import type { YBlock } from '../store/doc/index.js';
import type { Query } from '../store/doc/query.js';
import type { Doc, GetBlocksOptions, Workspace } from '../store/workspace.js';
import type { YBlock } from '../model/block/types.js';
import { Blocks } from '../model/blocks/blocks.js';
import type { Query } from '../model/blocks/query.js';
import type { Doc, GetBlocksOptions, Workspace } from '../model/index.js';
import type { AwarenessStore } from '../yjs/index.js';
type DocOptions = {

View File

@@ -1,16 +1,18 @@
import { Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js';
import { createYProxy } from '../reactive/proxy.js';
import type {
DocMeta,
DocsPropertiesMeta,
Workspace,
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[];
properties?: DocsPropertiesMeta;
workspaceVersion?: number;

View File

@@ -16,14 +16,14 @@ import merge from 'lodash.merge';
import { Awareness } from 'y-protocols/awareness.js';
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 Blocks,
type CreateBlocksOptions,
type GetBlocksOptions,
type Workspace,
type WorkspaceMeta,
} from '../store/index.js';
import { type IdGenerator, nanoid } from '../utils/id-generator.js';
import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js';
import { TestDoc } from './test-doc.js';

View File

@@ -1,7 +1,10 @@
import type { BlockModel, InternalPrimitives } from '../schema/index.js';
import { internalPrimitives } from '../schema/index.js';
import type { BlockModel } from '../model/block/block-model.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 { DraftModel } from './draft.js';
import { fromJSON, toJSON } from './json.js';
import type { BlockSnapshot } from './type.js';

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
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 { DraftModel } from './draft.js';
import type { Slice } from './slice.js';
import type {
BlockSnapshot,

View File

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

View File

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

View File

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

View File

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

View File

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