@blocksuite/store
Advanced tools
Comparing version 0.4.0-20230126161407-c60bfba to 0.4.0-20230127234317-bfe4022
@@ -6,6 +6,76 @@ /// <reference types="@blocksuite/global" /> | ||
import type * as Y from 'yjs'; | ||
import { z } from 'zod'; | ||
export declare const BlockSchema: z.ZodObject<{ | ||
version: z.ZodNumber; | ||
model: z.ZodObject<{ | ||
flavour: z.ZodString; | ||
tag: z.ZodObject<{ | ||
_$litStatic$: z.ZodString; | ||
r: z.ZodSymbol; | ||
}, "strip", z.ZodTypeAny, { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}, { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}>; | ||
state: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodAny>>; | ||
}, "strip", z.ZodTypeAny, { | ||
flavour: string; | ||
tag: { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}; | ||
state: (...args: unknown[]) => Record<string, any>; | ||
}, { | ||
flavour: string; | ||
tag: { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}; | ||
state: (...args: unknown[]) => Record<string, any>; | ||
}>; | ||
}, "strip", z.ZodTypeAny, { | ||
version: number; | ||
model: { | ||
flavour: string; | ||
tag: { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}; | ||
state: (...args: unknown[]) => Record<string, any>; | ||
}; | ||
}, { | ||
version: number; | ||
model: { | ||
flavour: string; | ||
tag: { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}; | ||
state: (...args: unknown[]) => Record<string, any>; | ||
}; | ||
}>; | ||
interface StaticValue { | ||
_$litStatic$: string; | ||
r: unknown; | ||
r: symbol; | ||
} | ||
export type SchemaToModel<Schema extends { | ||
model: { | ||
state: () => Record<string, unknown>; | ||
flavour: string; | ||
}; | ||
}> = BaseBlockModel & ReturnType<Schema['model']['state']> & { | ||
flavour: Schema['model']['flavour']; | ||
}; | ||
export declare function defineBlockSchema<Flavour extends string, State extends Record<string, unknown>, Metadata extends Readonly<{ | ||
version: number; | ||
tag: StaticValue; | ||
}>>(flavour: Flavour, state: () => State, metadata: Metadata): { | ||
version: number; | ||
model: { | ||
state: () => State; | ||
flavour: Flavour; | ||
} & Metadata; | ||
}; | ||
export declare class BaseBlockModel<Props = unknown> implements BlockSuiteInternal.IBaseBlockProps { | ||
@@ -20,3 +90,3 @@ static version: number; | ||
childMap: Map<string, number>; | ||
type: string; | ||
type?: string; | ||
children: BaseBlockModel[]; | ||
@@ -23,0 +93,0 @@ tags?: Y.Map<Y.Map<unknown>>; |
import { Signal } from '@blocksuite/global/utils'; | ||
import { z } from 'zod'; | ||
const FlavourSchema = z.string(); | ||
const TagSchema = z.object({ | ||
_$litStatic$: z.string(), | ||
r: z.symbol(), | ||
}); | ||
export const BlockSchema = z.object({ | ||
version: z.number(), | ||
model: z.object({ | ||
flavour: FlavourSchema, | ||
tag: TagSchema, | ||
state: z.function().returns(z.record(z.any())), | ||
}), | ||
}); | ||
export function defineBlockSchema(flavour, state, metadata) { | ||
const schema = { | ||
version: metadata.version, | ||
model: { | ||
flavour, | ||
tag: metadata.tag, | ||
state, | ||
}, | ||
}; | ||
BlockSchema.parse(schema); | ||
return schema; | ||
} | ||
export class BaseBlockModel { | ||
@@ -3,0 +29,0 @@ constructor(page, props) { |
@@ -5,6 +5,6 @@ export * from './space.js'; | ||
export * from './awareness.js'; | ||
export * from './blob/index.js'; | ||
export * from './persistence/blob/index.js'; | ||
export * from './text-adapter.js'; | ||
export * from '@blocksuite/global/utils'; | ||
export * from './doc-providers.js'; | ||
export * from './persistence/doc/index.js'; | ||
export * from './workspace/index.js'; | ||
@@ -11,0 +11,0 @@ export * as Utils from './utils/utils.js'; |
@@ -6,6 +6,6 @@ /// <reference types="@blocksuite/global" /> | ||
export * from './awareness.js'; | ||
export * from './blob/index.js'; | ||
export * from './persistence/blob/index.js'; | ||
export * from './text-adapter.js'; | ||
export * from '@blocksuite/global/utils'; | ||
export * from './doc-providers.js'; | ||
export * from './persistence/doc/index.js'; | ||
export * from './workspace/index.js'; | ||
@@ -12,0 +12,0 @@ export * as Utils from './utils/utils.js'; |
@@ -5,6 +5,6 @@ /// <reference types="@blocksuite/global" /> | ||
import { Awareness } from 'y-protocols/awareness.js'; | ||
import type { DocProvider, DocProviderConstructor } from './doc-providers.js'; | ||
import type { DocProvider, DocProviderConstructor } from './persistence/doc/index.js'; | ||
import { BlockSuiteDoc } from './yjs/index.js'; | ||
import { AwarenessStore, RawAwarenessState } from './awareness.js'; | ||
import type { BlobOptionsGetter } from './blob/index.js'; | ||
import type { BlobOptionsGetter } from './persistence/blob/index.js'; | ||
export interface SerializedStore { | ||
@@ -11,0 +11,0 @@ [key: string]: { |
@@ -9,3 +9,3 @@ /// <reference types="@blocksuite/global" /> | ||
export declare function initInternalProps(yBlock: YBlock, props: Partial<BlockProps>): void; | ||
export declare function syncBlockProps(yBlock: YBlock, props: Partial<BlockProps>, ignoredKeys: Set<string>): void; | ||
export declare function syncBlockProps(defaultState: Record<string, unknown>, yBlock: YBlock, props: Partial<BlockProps>, ignoredKeys: Set<string>): void; | ||
export declare function trySyncTextProp(splitSet: Set<Text | PrelimText>, yBlock: YBlock, text?: TextType | void): void; | ||
@@ -12,0 +12,0 @@ export declare function toBlockProps(yBlock: YBlock): Partial<BlockProps>; |
@@ -27,61 +27,35 @@ import * as Y from 'yjs'; | ||
} | ||
export function syncBlockProps(yBlock, props, ignoredKeys) { | ||
export function syncBlockProps( | ||
// schema: z.infer<typeof BlockSchema>, | ||
defaultState, yBlock, props, ignoredKeys) { | ||
Object.keys(props).forEach(key => { | ||
if (SYS_KEYS.has(key) || ignoredKeys.has(key)) | ||
return; | ||
const value = props[key]; | ||
// TODO use schema | ||
if (key === 'text') | ||
return; | ||
if (!isPrimitive(props[key]) && !Array.isArray(props[key])) { | ||
if (!isPrimitive(value) && !Array.isArray(value)) { | ||
throw new Error('Only top level primitives are supported for now'); | ||
} | ||
// TODO compare with current yBlock value | ||
if (props[key] !== undefined) { | ||
yBlock.set('prop:' + key, props[key]); | ||
if (value !== undefined) { | ||
if (Array.isArray(value)) { | ||
yBlock.set(`prop:${key}`, Y.Array.from(value)); | ||
} | ||
else { | ||
yBlock.set(`prop:${key}`, value); | ||
} | ||
} | ||
}); | ||
// TODO use schema | ||
if (props.flavour === 'affine:paragraph' && | ||
!props.type && | ||
!yBlock.has('prop:type')) { | ||
yBlock.set('prop:type', 'text'); | ||
} | ||
if (props.flavour === 'affine:list' && !yBlock.has('prop:type')) { | ||
yBlock.set('prop:type', props.type ?? 'bulleted'); | ||
} | ||
if (props.flavour === 'affine:list' && !yBlock.has('prop:checked')) { | ||
yBlock.set('prop:checked', props.checked ?? false); | ||
} | ||
if (props.flavour === 'affine:frame' && !yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,720,480]'); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:width')) { | ||
yBlock.set('prop:width', props.width ?? 20); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:sourceId')) { | ||
yBlock.set('prop:sourceId', props.sourceId ?? ''); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:caption')) { | ||
yBlock.set('prop:caption', props.caption ?? ''); | ||
} | ||
if (props.flavour === 'affine:shape') { | ||
if (!yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,50,50]'); | ||
// set default value | ||
Object.entries(defaultState).forEach(([key, value]) => { | ||
if (!yBlock.has(`prop:${key}`)) { | ||
if (Array.isArray(value)) { | ||
yBlock.set(`prop:${key}`, Y.Array.from(value)); | ||
} | ||
else { | ||
yBlock.set(`prop:${key}`, value); | ||
} | ||
} | ||
if (!yBlock.has('prop:type')) { | ||
yBlock.set('prop:type', props.type ?? 'rectangle'); | ||
} | ||
if (!yBlock.has('prop:color')) { | ||
yBlock.set('prop:color', props.color ?? 'black'); | ||
} | ||
} | ||
if (props.flavour === 'affine:database') { | ||
if (!yBlock.has('prop:columns')) { | ||
const columns = Y.Array.from(props.columns ?? []); | ||
yBlock.set('prop:columns', columns); | ||
} | ||
if (!yBlock.has('prop:title')) { | ||
yBlock.set('prop:title', ''); | ||
} | ||
} | ||
}); | ||
} | ||
@@ -88,0 +62,0 @@ export function trySyncTextProp(splitSet, yBlock, text) { |
@@ -55,3 +55,3 @@ /// <reference types="@blocksuite/global" /> | ||
get root(): BaseBlockModel<unknown> | null; | ||
get rootLayer(): BaseBlockModel<unknown> | null; | ||
get surface(): BaseBlockModel<unknown> | null; | ||
/** @internal used for getting surface block elements for phasor */ | ||
@@ -58,0 +58,0 @@ get ySurfaceContainer(): Y.Map<unknown>; |
@@ -131,3 +131,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
} | ||
get rootLayer() { | ||
get surface() { | ||
return Array.isArray(this._root) ? this._root[1] : null; | ||
@@ -137,4 +137,4 @@ } | ||
get ySurfaceContainer() { | ||
assertExists(this.rootLayer); | ||
const ySurface = this._yBlocks.get(this.rootLayer.id); | ||
assertExists(this.surface); | ||
const ySurface = this._yBlocks.get(this.surface.id); | ||
if (ySurface?.has('elements')) { | ||
@@ -277,7 +277,2 @@ return ySurface.get('elements'); | ||
} | ||
// if (blockProps.flavour === 'affine:shape') { | ||
// if (parent != null || parentIndex != null) { | ||
// throw new Error('Shape block should only be appear under page'); | ||
// } | ||
// } | ||
const clonedProps = { flavour, ...blockProps }; | ||
@@ -288,5 +283,9 @@ const id = this._idGenerator(); | ||
const yBlock = new Y.Map(); | ||
// set the yBlock at the very beginning, otherwise yBlock will be always empty | ||
this._yBlocks.set(id, yBlock); | ||
assertValidChildren(this._yBlocks, clonedProps); | ||
initInternalProps(yBlock, clonedProps); | ||
syncBlockProps(yBlock, clonedProps, this._ignoredKeys); | ||
const defaultState = this.workspace.flavourInitialStateMap.get(flavour); | ||
assertExists(defaultState); | ||
syncBlockProps(defaultState, yBlock, clonedProps, this._ignoredKeys); | ||
trySyncTextProp(this._splitSet, yBlock, clonedProps.text); | ||
@@ -303,3 +302,2 @@ if (typeof parent === 'string') { | ||
} | ||
this._yBlocks.set(id, yBlock); | ||
}); | ||
@@ -375,3 +373,5 @@ return id; | ||
} | ||
syncBlockProps(yBlock, props, this._ignoredKeys); | ||
const defaultState = this.workspace.flavourInitialStateMap.get(model.flavour); | ||
assertExists(defaultState); | ||
syncBlockProps(defaultState, yBlock, props, this._ignoredKeys); | ||
}); | ||
@@ -506,4 +506,4 @@ } | ||
_createBlockModel(props) { | ||
const BlockModelCtor = this.workspace.flavourMap.get(props.flavour); | ||
if (!BlockModelCtor) { | ||
const schema = this.workspace.flavourSchemaMap.get(props.flavour); | ||
if (!schema) { | ||
throw new Error(`Block flavour ${props.flavour} is not registered`); | ||
@@ -514,3 +514,11 @@ } | ||
} | ||
const blockModel = new BlockModelCtor(this, props); | ||
const blockModel = new BaseBlockModel(this, props); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
blockModel.flavour = schema.model.flavour; | ||
blockModel.tag = schema.model.tag; | ||
const state = schema.model.state(); | ||
Object.entries(state).forEach(([key, value]) => { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
blockModel[key] = props[key] ?? value; | ||
}); | ||
return blockModel; | ||
@@ -545,2 +553,7 @@ } | ||
} | ||
// todo: use schema | ||
if (model.flavour === 'affine:database') { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
model.columns = yBlock.get('prop:columns').toArray(); | ||
} | ||
const yChildren = yBlock.get('sys:children'); | ||
@@ -613,5 +626,10 @@ if (yChildren instanceof Y.Array) { | ||
} | ||
// Update props | ||
const value = event.target.get(key); | ||
hasPropsUpdate = true; | ||
props[key.replace('prop:', '')] = event.target.get(key); | ||
if (value instanceof Y.Array) { | ||
props[key.replace('prop:', '')] = value.toArray(); | ||
} | ||
else { | ||
props[key.replace('prop:', '')] = value; | ||
} | ||
} | ||
@@ -618,0 +636,0 @@ if (hasPropsUpdate) { |
@@ -8,6 +8,7 @@ /// <reference types="@blocksuite/global" /> | ||
import { QueryContent } from './search.js'; | ||
import type { BaseBlockModel } from '../base.js'; | ||
import { BlobStorage, BlobOptionsGetter } from '../blob/index.js'; | ||
import { BlobStorage, BlobOptionsGetter } from '../persistence/blob/index.js'; | ||
import type { BlockSuiteDoc } from '../yjs/index.js'; | ||
import type { AwarenessStore } from '../awareness.js'; | ||
import type { z } from 'zod'; | ||
import { BlockSchema } from '../base.js'; | ||
export interface PageMeta { | ||
@@ -67,10 +68,21 @@ id: string; | ||
}; | ||
flavourMap: Map<string, typeof BaseBlockModel>; | ||
flavourSchemaMap: Map<string, { | ||
version: number; | ||
model: { | ||
flavour: string; | ||
tag: { | ||
_$litStatic$: string; | ||
r: symbol; | ||
}; | ||
state: (...args: unknown[]) => Record<string, any>; | ||
}; | ||
}>; | ||
flavourInitialStateMap: Map<string, Record<string, unknown>>; | ||
constructor(options: StoreOptions); | ||
get awarenessStore(): AwarenessStore; | ||
get providers(): import("../doc-providers.js").DocProvider[]; | ||
get providers(): import("../index.js").DocProvider[]; | ||
get blobs(): Promise<BlobStorage | null>; | ||
private get _pages(); | ||
get doc(): BlockSuiteDoc<import("../yjs/index.js").BlockSuiteDocData>; | ||
register(blockSchema: Record<string, typeof BaseBlockModel>): this; | ||
register(blockSchema: z.infer<typeof BlockSchema>[]): this; | ||
private _hasPage; | ||
@@ -77,0 +89,0 @@ getPage(pageId: string): Page | null; |
@@ -7,3 +7,4 @@ import * as Y from 'yjs'; | ||
import { Indexer } from './search.js'; | ||
import { getBlobStorage, } from '../blob/index.js'; | ||
import { getBlobStorage, } from '../persistence/blob/index.js'; | ||
import { BlockSchema } from '../base.js'; | ||
class WorkspaceMeta extends Space { | ||
@@ -104,4 +105,4 @@ constructor(id, doc, awarenessStore) { | ||
const versions = this.proxy.versions; | ||
workspace.flavourMap.forEach((model, flavour) => { | ||
versions.set(flavour, model.version); | ||
workspace.flavourSchemaMap.forEach((schema, flavour) => { | ||
versions.set(flavour, schema.version); | ||
}); | ||
@@ -121,3 +122,3 @@ } | ||
const dataVersion = versions[dataFlavour]; | ||
const editorVersion = workspace.flavourMap.get(dataFlavour)?.version; | ||
const editorVersion = workspace.flavourSchemaMap.get(dataFlavour)?.version; | ||
if (!editorVersion) { | ||
@@ -162,3 +163,4 @@ throw new Error(`Editor missing ${dataFlavour} flavour. Please make sure this block flavour is registered.`); | ||
this._blobOptionsGetter = (k) => ({ api: '/api/workspace' }[k]); | ||
this.flavourMap = new Map(); | ||
this.flavourSchemaMap = new Map(); | ||
this.flavourInitialStateMap = new Map(); | ||
this._store = new Store(options); | ||
@@ -204,4 +206,6 @@ this._indexer = new Indexer(this.doc); | ||
register(blockSchema) { | ||
Object.keys(blockSchema).forEach(key => { | ||
this.flavourMap.set(key, blockSchema[key]); | ||
blockSchema.forEach(schema => { | ||
BlockSchema.parse(schema); | ||
this.flavourSchemaMap.set(schema.model.flavour, schema); | ||
this.flavourInitialStateMap.set(schema.model.flavour, schema.model.state()); | ||
}); | ||
@@ -208,0 +212,0 @@ return this; |
{ | ||
"name": "@blocksuite/store", | ||
"version": "0.4.0-20230126161407-c60bfba", | ||
"version": "0.4.0-20230127234317-bfe4022", | ||
"description": "BlockSuite data store built for general purpose state management.", | ||
@@ -11,3 +11,3 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@blocksuite/global": "0.4.0-20230126161407-c60bfba", | ||
"@blocksuite/global": "0.4.0-20230127234317-bfe4022", | ||
"@types/flexsearch": "^0.7.3", | ||
@@ -22,3 +22,4 @@ "buffer": "^6.0.3", | ||
"y-protocols": "^1.0.5", | ||
"y-webrtc": "^10.2.4" | ||
"y-webrtc": "^10.2.4", | ||
"zod": "^3.20.2" | ||
}, | ||
@@ -25,0 +26,0 @@ "devDependencies": { |
@@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/no-restricted-imports */ | ||
// Use manual per-module import/export to support vitest environment on Node.js | ||
import { PageBlockModel } from '../../../blocks/src/page-block/page-model.js'; | ||
import { ParagraphBlockModel } from '../../../blocks/src/paragraph-block/paragraph-model.js'; | ||
import { ListBlockModel } from '../../../blocks/src/list-block/list-model.js'; | ||
import { FrameBlockModel } from '../../../blocks/src/frame-block/frame-model.js'; | ||
import { DividerBlockModel } from '../../../blocks/src/divider-block/divider-model.js'; | ||
import { PageBlockModelSchema } from '../../../blocks/src/page-block/page-model.js'; | ||
import { ParagraphBlockModelSchema } from '../../../blocks/src/paragraph-block/paragraph-model.js'; | ||
import { ListBlockModelSchema } from '../../../blocks/src/list-block/list-model.js'; | ||
import { FrameBlockModelSchema } from '../../../blocks/src/frame-block/frame-model.js'; | ||
import { DividerBlockModelSchema } from '../../../blocks/src/divider-block/divider-model.js'; | ||
import type { PageMeta } from '../workspace/index.js'; | ||
@@ -24,9 +24,9 @@ import { assertExists } from './test-utils-dom.js'; | ||
// Create BlockSchema manually | ||
export const BlockSchema = { | ||
'affine:paragraph': ParagraphBlockModel, | ||
'affine:page': PageBlockModel, | ||
'affine:list': ListBlockModel, | ||
'affine:frame': FrameBlockModel, | ||
'affine:divider': DividerBlockModel, | ||
} as const; | ||
export const BlockSchema = [ | ||
ParagraphBlockModelSchema, | ||
PageBlockModelSchema, | ||
ListBlockModelSchema, | ||
FrameBlockModelSchema, | ||
DividerBlockModelSchema, | ||
]; | ||
@@ -101,2 +101,3 @@ function serialize(page: Page) { | ||
'meta:tagSchema': {}, | ||
'prop:title': '', | ||
'sys:children': [], | ||
@@ -137,2 +138,3 @@ 'sys:flavour': 'affine:page', | ||
'sys:id': '0', | ||
'prop:title': '', | ||
}, | ||
@@ -154,3 +156,6 @@ '1': { | ||
const block = await waitOnce(page.signals.rootAdded); | ||
assert.ok(block instanceof BlockSchema['affine:page']); | ||
if (Array.isArray(block)) { | ||
throw new Error(''); | ||
} | ||
assert.equal(block.flavour, 'affine:page'); | ||
}); | ||
@@ -164,6 +169,9 @@ | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
assert.ok(root instanceof BlockSchema['affine:page']); | ||
if (Array.isArray(root)) { | ||
throw new Error(''); | ||
} | ||
assert.equal(root.flavour, 'affine:page'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
assert.ok(root.children[0] instanceof BlockSchema['affine:paragraph']); | ||
assert.equal(root.children[0].flavour, 'affine:paragraph'); | ||
assert.equal(root.childMap.get('1'), 0); | ||
@@ -264,2 +272,3 @@ | ||
'sys:id': '0', | ||
'prop:title': '', | ||
}, | ||
@@ -284,2 +293,3 @@ }); | ||
'meta:tagSchema': {}, | ||
'prop:title': '', | ||
'sys:children': ['1'], | ||
@@ -305,2 +315,3 @@ 'sys:flavour': 'affine:page', | ||
'meta:tagSchema': {}, | ||
'prop:title': '', | ||
'sys:children': [], | ||
@@ -325,3 +336,3 @@ 'sys:flavour': 'affine:page', | ||
const text = page.getBlockById('2') as BaseBlockModel; | ||
assert.ok(text instanceof BlockSchema['affine:paragraph']); | ||
assert.equal(text.flavour, 'affine:paragraph'); | ||
assert.equal(root.children.indexOf(text), 1); | ||
@@ -398,3 +409,5 @@ | ||
expect(workspace.exportJSX()).toMatchInlineSnapshot(/* xml */ ` | ||
<affine:page> | ||
<affine:page | ||
prop:title="" | ||
> | ||
<affine:paragraph | ||
@@ -401,0 +414,0 @@ prop:type="text" |
@@ -5,9 +5,76 @@ import type { Page } from './workspace/index.js'; | ||
import type * as Y from 'yjs'; | ||
import { z } from 'zod'; | ||
const FlavourSchema = z.string(); | ||
const TagSchema = z.object({ | ||
_$litStatic$: z.string(), | ||
r: z.symbol(), | ||
}); | ||
export const BlockSchema = z.object({ | ||
version: z.number(), | ||
model: z.object({ | ||
flavour: FlavourSchema, | ||
tag: TagSchema, | ||
state: z.function().returns(z.record(z.any())), | ||
}), | ||
}); | ||
// ported from lit | ||
interface StaticValue { | ||
_$litStatic$: string; | ||
r: unknown; | ||
r: symbol; | ||
} | ||
export type SchemaToModel< | ||
Schema extends { | ||
model: { | ||
state: () => Record<string, unknown>; | ||
flavour: string; | ||
}; | ||
} | ||
> = BaseBlockModel & | ||
ReturnType<Schema['model']['state']> & { | ||
flavour: Schema['model']['flavour']; | ||
}; | ||
export function defineBlockSchema< | ||
Flavour extends string, | ||
State extends Record<string, unknown>, | ||
Metadata extends Readonly<{ | ||
version: number; | ||
tag: StaticValue; | ||
}> | ||
>( | ||
flavour: Flavour, | ||
state: () => State, | ||
metadata: Metadata | ||
): { | ||
version: number; | ||
model: { | ||
state: () => State; | ||
flavour: Flavour; | ||
} & Metadata; | ||
}; | ||
export function defineBlockSchema( | ||
flavour: string, | ||
state: () => Record<string, unknown>, | ||
metadata: { | ||
version: number; | ||
tag: StaticValue; | ||
} | ||
): z.infer<typeof BlockSchema> { | ||
const schema = { | ||
version: metadata.version, | ||
model: { | ||
flavour, | ||
tag: metadata.tag, | ||
state, | ||
}, | ||
} satisfies z.infer<typeof BlockSchema>; | ||
BlockSchema.parse(schema); | ||
return schema; | ||
} | ||
export class BaseBlockModel<Props = unknown> | ||
@@ -26,3 +93,3 @@ implements BlockSuiteInternal.IBaseBlockProps | ||
type!: string; | ||
type?: string; | ||
children: BaseBlockModel[]; | ||
@@ -29,0 +96,0 @@ // TODO use schema |
@@ -6,6 +6,6 @@ /// <reference types="@blocksuite/global" /> | ||
export * from './awareness.js'; | ||
export * from './blob/index.js'; | ||
export * from './persistence/blob/index.js'; | ||
export * from './text-adapter.js'; | ||
export * from '@blocksuite/global/utils'; | ||
export * from './doc-providers.js'; | ||
export * from './persistence/doc/index.js'; | ||
export * from './workspace/index.js'; | ||
@@ -12,0 +12,0 @@ export * as Utils from './utils/utils.js'; |
import type { Space } from './space.js'; | ||
import type { IdGenerator } from './utils/id-generator.js'; | ||
import { Awareness } from 'y-protocols/awareness.js'; | ||
import type { DocProvider, DocProviderConstructor } from './doc-providers.js'; | ||
import type { | ||
DocProvider, | ||
DocProviderConstructor, | ||
} from './persistence/doc/index.js'; | ||
import { serializeYDoc, yDocToJSXNode } from './utils/jsx.js'; | ||
@@ -15,3 +18,3 @@ import { | ||
import { AwarenessStore, RawAwarenessState } from './awareness.js'; | ||
import type { BlobOptionsGetter } from './blob/index.js'; | ||
import type { BlobOptionsGetter } from './persistence/blob/index.js'; | ||
@@ -18,0 +21,0 @@ export interface SerializedStore { |
@@ -44,2 +44,4 @@ import * as Y from 'yjs'; | ||
export function syncBlockProps( | ||
// schema: z.infer<typeof BlockSchema>, | ||
defaultState: Record<string, unknown>, | ||
yBlock: YBlock, | ||
@@ -51,62 +53,29 @@ props: Partial<BlockProps>, | ||
if (SYS_KEYS.has(key) || ignoredKeys.has(key)) return; | ||
const value = props[key]; | ||
// TODO use schema | ||
if (key === 'text') return; | ||
if (!isPrimitive(props[key]) && !Array.isArray(props[key])) { | ||
if (!isPrimitive(value) && !Array.isArray(value)) { | ||
throw new Error('Only top level primitives are supported for now'); | ||
} | ||
// TODO compare with current yBlock value | ||
if (props[key] !== undefined) { | ||
yBlock.set('prop:' + key, props[key]); | ||
if (value !== undefined) { | ||
if (Array.isArray(value)) { | ||
yBlock.set(`prop:${key}`, Y.Array.from(value)); | ||
} else { | ||
yBlock.set(`prop:${key}`, value); | ||
} | ||
} | ||
}); | ||
// TODO use schema | ||
if ( | ||
props.flavour === 'affine:paragraph' && | ||
!props.type && | ||
!yBlock.has('prop:type') | ||
) { | ||
yBlock.set('prop:type', 'text'); | ||
} | ||
if (props.flavour === 'affine:list' && !yBlock.has('prop:type')) { | ||
yBlock.set('prop:type', props.type ?? 'bulleted'); | ||
} | ||
if (props.flavour === 'affine:list' && !yBlock.has('prop:checked')) { | ||
yBlock.set('prop:checked', props.checked ?? false); | ||
} | ||
if (props.flavour === 'affine:frame' && !yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,720,480]'); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:width')) { | ||
yBlock.set('prop:width', props.width ?? 20); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:sourceId')) { | ||
yBlock.set('prop:sourceId', props.sourceId ?? ''); | ||
} | ||
if (props.flavour === 'affine:embed' && !yBlock.has('prop:caption')) { | ||
yBlock.set('prop:caption', props.caption ?? ''); | ||
} | ||
if (props.flavour === 'affine:shape') { | ||
if (!yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,50,50]'); | ||
// set default value | ||
Object.entries(defaultState).forEach(([key, value]) => { | ||
if (!yBlock.has(`prop:${key}`)) { | ||
if (Array.isArray(value)) { | ||
yBlock.set(`prop:${key}`, Y.Array.from(value)); | ||
} else { | ||
yBlock.set(`prop:${key}`, value); | ||
} | ||
} | ||
if (!yBlock.has('prop:type')) { | ||
yBlock.set('prop:type', props.type ?? 'rectangle'); | ||
} | ||
if (!yBlock.has('prop:color')) { | ||
yBlock.set('prop:color', props.color ?? 'black'); | ||
} | ||
} | ||
if (props.flavour === 'affine:database') { | ||
if (!yBlock.has('prop:columns')) { | ||
const columns = Y.Array.from(props.columns ?? []); | ||
yBlock.set('prop:columns', columns); | ||
} | ||
if (!yBlock.has('prop:title')) { | ||
yBlock.set('prop:title', ''); | ||
} | ||
} | ||
}); | ||
} | ||
@@ -113,0 +82,0 @@ |
@@ -124,3 +124,3 @@ import * as Y from 'yjs'; | ||
get rootLayer() { | ||
get surface() { | ||
return Array.isArray(this._root) ? this._root[1] : null; | ||
@@ -131,4 +131,4 @@ } | ||
get ySurfaceContainer() { | ||
assertExists(this.rootLayer); | ||
const ySurface = this._yBlocks.get(this.rootLayer.id); | ||
assertExists(this.surface); | ||
const ySurface = this._yBlocks.get(this.surface.id); | ||
if (ySurface?.has('elements')) { | ||
@@ -338,8 +338,2 @@ return ySurface.get('elements') as Y.Map<unknown>; | ||
// if (blockProps.flavour === 'affine:shape') { | ||
// if (parent != null || parentIndex != null) { | ||
// throw new Error('Shape block should only be appear under page'); | ||
// } | ||
// } | ||
const clonedProps: Partial<BlockProps> = { flavour, ...blockProps }; | ||
@@ -351,6 +345,10 @@ const id = this._idGenerator(); | ||
const yBlock = new Y.Map() as YBlock; | ||
// set the yBlock at the very beginning, otherwise yBlock will be always empty | ||
this._yBlocks.set(id, yBlock); | ||
assertValidChildren(this._yBlocks, clonedProps); | ||
initInternalProps(yBlock, clonedProps); | ||
syncBlockProps(yBlock, clonedProps, this._ignoredKeys); | ||
const defaultState = this.workspace.flavourInitialStateMap.get(flavour); | ||
assertExists(defaultState); | ||
syncBlockProps(defaultState, yBlock, clonedProps, this._ignoredKeys); | ||
trySyncTextProp(this._splitSet, yBlock, clonedProps.text); | ||
@@ -370,4 +368,2 @@ | ||
} | ||
this._yBlocks.set(id, yBlock); | ||
}); | ||
@@ -462,3 +458,7 @@ return id; | ||
syncBlockProps(yBlock, props, this._ignoredKeys); | ||
const defaultState = this.workspace.flavourInitialStateMap.get( | ||
model.flavour | ||
); | ||
assertExists(defaultState); | ||
syncBlockProps(defaultState, yBlock, props, this._ignoredKeys); | ||
}); | ||
@@ -672,4 +672,4 @@ } | ||
private _createBlockModel(props: Omit<BlockProps, 'children'>) { | ||
const BlockModelCtor = this.workspace.flavourMap.get(props.flavour); | ||
if (!BlockModelCtor) { | ||
const schema = this.workspace.flavourSchemaMap.get(props.flavour); | ||
if (!schema) { | ||
throw new Error(`Block flavour ${props.flavour} is not registered`); | ||
@@ -679,7 +679,14 @@ } else if (!props.id) { | ||
} | ||
const blockModel = new BlockModelCtor( | ||
const blockModel = new BaseBlockModel( | ||
this, | ||
props as PropsWithId<Omit<BlockProps, 'children'>> | ||
); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
blockModel.flavour = schema.model.flavour as any; | ||
blockModel.tag = schema.model.tag; | ||
const state = schema.model.state(); | ||
Object.entries(state).forEach(([key, value]) => { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(blockModel as any)[key] = props[key] ?? value; | ||
}); | ||
return blockModel; | ||
@@ -720,2 +727,10 @@ } | ||
// todo: use schema | ||
if (model.flavour === 'affine:database') { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(model as any).columns = ( | ||
yBlock.get('prop:columns') as Y.Array<unknown> | ||
).toArray(); | ||
} | ||
const yChildren = yBlock.get('sys:children'); | ||
@@ -796,5 +811,9 @@ if (yChildren instanceof Y.Array) { | ||
} | ||
// Update props | ||
const value = event.target.get(key); | ||
hasPropsUpdate = true; | ||
props[key.replace('prop:', '')] = event.target.get(key); | ||
if (value instanceof Y.Array) { | ||
props[key.replace('prop:', '')] = value.toArray(); | ||
} else { | ||
props[key.replace('prop:', '')] = value; | ||
} | ||
} | ||
@@ -801,0 +820,0 @@ |
@@ -7,3 +7,2 @@ import * as Y from 'yjs'; | ||
import { Indexer, QueryContent } from './search.js'; | ||
import type { BaseBlockModel } from '../base.js'; | ||
import { | ||
@@ -13,5 +12,7 @@ BlobStorage, | ||
getBlobStorage, | ||
} from '../blob/index.js'; | ||
} from '../persistence/blob/index.js'; | ||
import type { BlockSuiteDoc } from '../yjs/index.js'; | ||
import type { AwarenessStore } from '../awareness.js'; | ||
import type { z } from 'zod'; | ||
import { BlockSchema } from '../base.js'; | ||
@@ -130,4 +131,4 @@ export interface PageMeta { | ||
const versions = this.proxy.versions; | ||
workspace.flavourMap.forEach((model, flavour) => { | ||
versions.set(flavour, model.version); | ||
workspace.flavourSchemaMap.forEach((schema, flavour) => { | ||
versions.set(flavour, schema.version); | ||
}); | ||
@@ -152,3 +153,4 @@ } | ||
const dataVersion = versions[dataFlavour] as number; | ||
const editorVersion = workspace.flavourMap.get(dataFlavour)?.version; | ||
const editorVersion = | ||
workspace.flavourSchemaMap.get(dataFlavour)?.version; | ||
if (!editorVersion) { | ||
@@ -239,3 +241,4 @@ throw new Error( | ||
flavourMap = new Map<string, typeof BaseBlockModel>(); | ||
flavourSchemaMap = new Map<string, z.infer<typeof BlockSchema>>(); | ||
flavourInitialStateMap = new Map<string, Record<string, unknown>>(); | ||
@@ -290,5 +293,10 @@ constructor(options: StoreOptions) { | ||
register(blockSchema: Record<string, typeof BaseBlockModel>) { | ||
Object.keys(blockSchema).forEach(key => { | ||
this.flavourMap.set(key, blockSchema[key]); | ||
register(blockSchema: z.infer<typeof BlockSchema>[]) { | ||
blockSchema.forEach(schema => { | ||
BlockSchema.parse(schema); | ||
this.flavourSchemaMap.set(schema.model.flavour, schema); | ||
this.flavourInitialStateMap.set( | ||
schema.model.flavour, | ||
schema.model.state() | ||
); | ||
}); | ||
@@ -295,0 +303,0 @@ return this; |
@@ -9,4 +9,4 @@ { | ||
"include": ["./src"], | ||
"exclude": ["./src/__tests__", "./src/blob/__tests__"], | ||
"exclude": ["./src/__tests__", "src/persistence/blob/__tests__"], | ||
"references": [] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
504034
165
8244
13
+ Addedzod@^3.20.2
+ Added@blocksuite/global@0.4.0-20230127234317-bfe4022(transitive)
+ Addedzod@3.24.1(transitive)
- Removed@blocksuite/global@0.4.0-20230126161407-c60bfba(transitive)