@blocksuite/store
Advanced tools
Comparing version 0.5.0-alpha.1 to 0.5.0-alpha.2
{ | ||
"name": "@blocksuite/store", | ||
"version": "0.5.0-alpha.1", | ||
"version": "0.5.0-alpha.2", | ||
"description": "BlockSuite data store built for general purpose state management.", | ||
"main": "dist/index.js", | ||
"main": "src/index.ts", | ||
"type": "module", | ||
"scripts": { | ||
"serve": "PORT=4444 node node_modules/y-webrtc/bin/server.js", | ||
"build": "tsc", | ||
"test:unit": "vitest --run", | ||
"test:unit:coverage": "vitest run --coverage", | ||
"test:unit:ui": "vitest --ui", | ||
"test:e2e": "playwright test", | ||
"test": "pnpm test:unit && pnpm test:e2e" | ||
}, | ||
"keywords": [], | ||
@@ -11,4 +20,4 @@ "author": "toeverything", | ||
"dependencies": { | ||
"@blocksuite/global": "0.5.0-alpha.1", | ||
"@blocksuite/virgo": "0.5.0-alpha.1", | ||
"@blocksuite/global": "workspace:*", | ||
"@blocksuite/virgo": "workspace:*", | ||
"@types/flexsearch": "^0.7.3", | ||
@@ -18,13 +27,13 @@ "buffer": "^6.0.3", | ||
"idb-keyval": "^6.2.0", | ||
"ky": "^0.33.2", | ||
"lib0": "^0.2.63", | ||
"ky": "^0.33.3", | ||
"lib0": "^0.2.68", | ||
"merge": "^2.1.1", | ||
"nanoid": "^4.0.1", | ||
"y-protocols": "^1.0.5", | ||
"y-webrtc": "^10.2.4", | ||
"zod": "^3.20.6" | ||
"y-webrtc": "^10.2.5", | ||
"zod": "^3.21.4" | ||
}, | ||
"devDependencies": { | ||
"lit": "^2.6.1", | ||
"yjs": "^13.5.48" | ||
"yjs": "^13.5.50" | ||
}, | ||
@@ -35,21 +44,17 @@ "peerDependencies": { | ||
"exports": { | ||
"./src/*": "./dist/*.js", | ||
".": { | ||
"module": "./dist/index.js", | ||
"import": "./dist/index.js" | ||
} | ||
"./src/*": "./src/*.ts", | ||
".": "./src/index.ts" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"scripts": { | ||
"serve": "PORT=4444 node node_modules/y-webrtc/bin/server.js", | ||
"build": "tsc", | ||
"test:unit": "vitest --run", | ||
"test:unit:coverage": "vitest run --coverage", | ||
"test:unit:ui": "vitest --ui", | ||
"test:e2e": "playwright test", | ||
"test": "pnpm test:unit && pnpm test:e2e" | ||
}, | ||
"types": "dist/index.d.ts" | ||
} | ||
"access": "public", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"exports": { | ||
"./src/*": "./dist/*.js", | ||
".": { | ||
"module": "./dist/index.js", | ||
"import": "./dist/index.js" | ||
} | ||
} | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
import { expect, Page } from '@playwright/test'; | ||
import { expect, type Page } from '@playwright/test'; | ||
@@ -3,0 +3,0 @@ import type { TestResult } from './test-utils-dom.js'; |
/* eslint-disable @typescript-eslint/no-restricted-imports */ | ||
// checkout https://vitest.dev/guide/debugging.html for debugging tests | ||
import type { Slot } from '@blocksuite/global/utils'; | ||
import { assert, describe, expect, it } from 'vitest'; | ||
import { DividerBlockModelSchema } from '../../../blocks/src/divider-block/divider-model.js'; | ||
import { FrameBlockModelSchema } from '../../../blocks/src/frame-block/frame-model.js'; | ||
import { ListBlockModelSchema } from '../../../blocks/src/list-block/list-model.js'; | ||
import { DividerBlockSchema } from '../../../blocks/src/divider-block/divider-model.js'; | ||
import { FrameBlockSchema } from '../../../blocks/src/frame-block/frame-model.js'; | ||
import { ListBlockSchema } from '../../../blocks/src/list-block/list-model.js'; | ||
// Use manual per-module import/export to support vitest environment on Node.js | ||
import { PageBlockModelSchema } from '../../../blocks/src/page-block/page-model.js'; | ||
import { ParagraphBlockModelSchema } from '../../../blocks/src/paragraph-block/paragraph-model.js'; | ||
import { BaseBlockModel, Generator, Page, Workspace } from '../index.js'; | ||
import { PageBlockSchema } from '../../../blocks/src/page-block/page-model.js'; | ||
import { ParagraphBlockSchema } from '../../../blocks/src/paragraph-block/paragraph-model.js'; | ||
import type { Slot } from '../../../global/src/utils/slot.js'; | ||
import type { BaseBlockModel, Page } from '../index.js'; | ||
import { Generator, Workspace } from '../index.js'; | ||
import type { PageMeta } from '../workspace/index.js'; | ||
import { assertExists } from './test-utils-dom.js'; | ||
@@ -22,11 +22,14 @@ function createTestOptions() { | ||
// Create BlockSchema manually | ||
export const BlockSchema = [ | ||
ParagraphBlockModelSchema, | ||
PageBlockModelSchema, | ||
ListBlockModelSchema, | ||
FrameBlockModelSchema, | ||
DividerBlockModelSchema, | ||
export const BlockSchemas = [ | ||
ParagraphBlockSchema, | ||
PageBlockSchema, | ||
ListBlockSchema, | ||
FrameBlockSchema, | ||
DividerBlockSchema, | ||
]; | ||
const defaultPageId = 'page0'; | ||
const spaceId = `space:${defaultPageId}`; | ||
const spaceMetaId = 'space:meta'; | ||
function serialize(page: Page) { | ||
@@ -40,33 +43,16 @@ return page.doc.toJSON(); | ||
async function createRoot(page: Page) { | ||
queueMicrotask(() => | ||
page.addBlockByFlavour('affine:page', { | ||
title: new page.Text(), | ||
}) | ||
); | ||
const root = await waitOnce(page.slots.rootAdded); | ||
return root; | ||
function createRoot(page: Page) { | ||
page.addBlock('affine:page'); | ||
if (!page.root) throw new Error('root not found'); | ||
return page.root; | ||
} | ||
async function createPage(workspace: Workspace, pageId = 'page0') { | ||
queueMicrotask(() => workspace.createPage(pageId)); | ||
await waitOnce(workspace.slots.pageAdded); | ||
const page = workspace.getPage(pageId); | ||
assertExists(page); | ||
return page; | ||
} | ||
async function createTestPage() { | ||
function createTestPage(pageId = defaultPageId, parentId?: string) { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const page = await createPage(workspace); | ||
return page; | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
return workspace.createPage(pageId, parentId); | ||
} | ||
const defaultPageId = 'page0'; | ||
const spaceId = `space:${defaultPageId}`; | ||
const spaceMetaId = 'space:meta'; | ||
describe.concurrent('basic', () => { | ||
it('can init workspace', async () => { | ||
describe('basic', () => { | ||
it('can init workspace', () => { | ||
const options = createTestOptions(); | ||
@@ -76,3 +62,3 @@ const workspace = new Workspace(options); | ||
const page = await createPage(workspace); | ||
const page = workspace.createPage('page0'); | ||
const actual = serialize(page); | ||
@@ -91,2 +77,3 @@ const actualPage = actual[spaceMetaId].pages[0] as PageMeta; | ||
id: 'page0', | ||
subpageIds: [], | ||
title: '', | ||
@@ -102,6 +89,38 @@ }, | ||
describe.concurrent('addBlock', () => { | ||
it('can add single model', async () => { | ||
const page = await createTestPage(); | ||
page.addBlockByFlavour('affine:page', { | ||
describe('pageMeta', () => { | ||
it('can create subpage', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const parentPage = workspace.createPage(defaultPageId); | ||
const subpage = workspace.createPage('subpage0', parentPage.id); | ||
assert.deepEqual(parentPage.meta.subpageIds, [subpage.id]); | ||
}); | ||
it('can shift subpage', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const page0 = workspace.createPage('page0'); | ||
const page1 = workspace.createPage('page1'); | ||
const page2 = workspace.createPage('page2'); | ||
assert.deepEqual( | ||
workspace.meta.pageMetas.map(m => m.id), | ||
['page0', 'page1', 'page2'] | ||
); | ||
workspace.shiftPage(page1.id, 0); | ||
assert.deepEqual( | ||
workspace.meta.pageMetas.map(m => m.id), | ||
['page1', 'page0', 'page2'] | ||
); | ||
}); | ||
}); | ||
describe('addBlock', () => { | ||
it('can add single model', () => { | ||
const page = createTestPage(); | ||
page.addBlock('affine:page', { | ||
title: new page.Text(), | ||
@@ -112,4 +131,4 @@ }); | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'prop:title': '', | ||
@@ -123,10 +142,10 @@ 'sys:children': [], | ||
it('can add model with props', async () => { | ||
const page = await createTestPage(); | ||
page.addBlockByFlavour('affine:page', { title: new page.Text('hello') }); | ||
it('can add model with props', () => { | ||
const page = createTestPage(); | ||
page.addBlock('affine:page', { title: new page.Text('hello') }); | ||
assert.deepEqual(serialize(page)[spaceId], { | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'sys:children': [], | ||
@@ -140,9 +159,9 @@ 'sys:flavour': 'affine:page', | ||
it('can add multi models', async () => { | ||
const page = await createTestPage(); | ||
page.addBlockByFlavour('affine:page', { | ||
it('can add multi models', () => { | ||
const page = createTestPage(); | ||
page.addBlock('affine:page', { | ||
title: new page.Text(), | ||
}); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlocksByFlavour([ | ||
page.addBlock('affine:paragraph'); | ||
page.addBlocks([ | ||
{ flavour: 'affine:paragraph', blockProps: { type: 'h1' } }, | ||
@@ -154,4 +173,4 @@ { flavour: 'affine:paragraph', blockProps: { type: 'h2' } }, | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'sys:children': ['1', '2', '3'], | ||
@@ -187,10 +206,10 @@ 'sys:flavour': 'affine:page', | ||
it('can observe slot events', async () => { | ||
const page = await createTestPage(); | ||
const page = createTestPage(); | ||
queueMicrotask(() => | ||
page.addBlockByFlavour('affine:page', { | ||
page.addBlock('affine:page', { | ||
title: new page.Text(), | ||
}) | ||
); | ||
const block = await waitOnce(page.slots.rootAdded); | ||
const block = (await waitOnce(page.slots.rootAdded)) as BaseBlockModel; | ||
if (Array.isArray(block)) { | ||
@@ -203,17 +222,12 @@ throw new Error(''); | ||
it('can add block to root', async () => { | ||
const page = await createTestPage(); | ||
const page = createTestPage(); | ||
queueMicrotask(() => | ||
page.addBlockByFlavour('affine:page', { | ||
title: new page.Text(), | ||
}) | ||
); | ||
const roots = await waitOnce(page.slots.rootAdded); | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
if (Array.isArray(root)) { | ||
throw new Error(''); | ||
} | ||
queueMicrotask(() => page.addBlock('affine:page')); | ||
await waitOnce(page.slots.rootAdded); | ||
const { root } = page; | ||
if (!root) throw new Error('root is null'); | ||
assert.equal(root.flavour, 'affine:page'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
assert.equal(root.children[0].flavour, 'affine:paragraph'); | ||
@@ -227,12 +241,12 @@ assert.equal(root.childMap.get('1'), 0); | ||
it('can add and remove multi pages', async () => { | ||
it('can add and remove multi pages', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const page0 = await createPage(workspace, 'page0'); | ||
const page1 = await createPage(workspace, 'page1'); | ||
// @ts-ignore | ||
const page0 = workspace.createPage('page0'); | ||
const page1 = workspace.createPage('page1'); | ||
// @ts-expect-error | ||
assert.equal(workspace._pages.size, 2); | ||
page0.addBlockByFlavour('affine:page', { | ||
page0.addBlock('affine:page', { | ||
title: new page0.Text(), | ||
@@ -251,5 +265,5 @@ }); | ||
it('can set page state', async () => { | ||
it('can set page state', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
workspace.createPage('page0'); | ||
@@ -271,3 +285,3 @@ | ||
let called = false; | ||
workspace.meta.pagesUpdated.on(() => { | ||
workspace.meta.pageMetasUpdated.on(() => { | ||
called = true; | ||
@@ -308,7 +322,7 @@ }); | ||
describe.concurrent('deleteBlock', () => { | ||
it('can delete single model', async () => { | ||
const page = await createTestPage(); | ||
describe('deleteBlock', () => { | ||
it('can delete single model', () => { | ||
const page = createTestPage(); | ||
page.addBlockByFlavour('affine:page', { | ||
page.addBlock('affine:page', { | ||
title: new page.Text(), | ||
@@ -318,4 +332,4 @@ }); | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'sys:children': [], | ||
@@ -328,12 +342,11 @@ 'sys:flavour': 'affine:page', | ||
page.deleteBlockById('0'); | ||
page.deleteBlock(page.root as BaseBlockModel); | ||
assert.deepEqual(serialize(page)[spaceId], {}); | ||
}); | ||
it('can delete model with parent', async () => { | ||
const page = await createTestPage(); | ||
const roots = await createRoot(page); | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
it('can delete model with parent', () => { | ||
const page = createTestPage(); | ||
const root = createRoot(page); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
@@ -343,4 +356,4 @@ // before delete | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'prop:title': '', | ||
@@ -365,4 +378,4 @@ 'sys:children': ['1'], | ||
'0': { | ||
'meta:tags': {}, | ||
'meta:tagSchema': {}, | ||
'ext:cells': {}, | ||
'ext:columnSchema': {}, | ||
'prop:title': '', | ||
@@ -378,10 +391,9 @@ 'sys:children': [], | ||
describe.concurrent('getBlock', () => { | ||
it('can get block by id', async () => { | ||
const page = await createTestPage(); | ||
const roots = await createRoot(page); | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
describe('getBlock', () => { | ||
it('can get block by id', () => { | ||
const page = createTestPage(); | ||
const root = createRoot(page); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
@@ -396,9 +408,8 @@ const text = page.getBlockById('2') as BaseBlockModel; | ||
it('can get parent', async () => { | ||
const page = await createTestPage(); | ||
const roots = await createRoot(page); | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
it('can get parent', () => { | ||
const page = createTestPage(); | ||
const root = createRoot(page); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
@@ -412,9 +423,8 @@ const result = page.getParent(root.children[1]) as BaseBlockModel; | ||
it('can get previous sibling', async () => { | ||
const page = await createTestPage(); | ||
const roots = await createRoot(page); | ||
const root = Array.isArray(roots) ? roots[0] : roots; | ||
it('can get previous sibling', () => { | ||
const page = createTestPage(); | ||
const root = createRoot(page); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
@@ -430,9 +440,9 @@ const result = page.getPreviousSibling(root.children[1]) as BaseBlockModel; | ||
// Inline snapshot is not supported under describe.parallel config | ||
describe('workspace.exportJSX works', async () => { | ||
it('workspace matches snapshot', async () => { | ||
describe('workspace.exportJSX works', () => { | ||
it('workspace matches snapshot', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const page = await createPage(workspace); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const page = workspace.createPage('page0'); | ||
page.addBlockByFlavour('affine:page', { title: new page.Text('hello') }); | ||
page.addBlock('affine:page', { title: new page.Text('hello') }); | ||
@@ -446,6 +456,6 @@ expect(workspace.exportJSX()).toMatchInlineSnapshot(` | ||
it('empty workspace matches snapshot', async () => { | ||
it('empty workspace matches snapshot', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
await createPage(workspace); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
workspace.createPage('page0'); | ||
@@ -455,12 +465,12 @@ expect(workspace.exportJSX()).toMatchInlineSnapshot('null'); | ||
it('workspace with multiple blocks children matches snapshot', async () => { | ||
it('workspace with multiple blocks children matches snapshot', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const page = await createPage(workspace); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const page = workspace.createPage('page0'); | ||
page.addBlockByFlavour('affine:page', { | ||
page.addBlock('affine:page', { | ||
title: new page.Text(), | ||
}); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlockByFlavour('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
page.addBlock('affine:paragraph'); | ||
@@ -480,11 +490,11 @@ expect(workspace.exportJSX()).toMatchInlineSnapshot(/* xml */ ` | ||
describe.concurrent('workspace.search works', async () => { | ||
it('workspace search matching', async () => { | ||
describe('workspace.search works', () => { | ||
it('workspace search matching', () => { | ||
const options = createTestOptions(); | ||
const workspace = new Workspace(options).register(BlockSchema); | ||
const page = await createPage(workspace); | ||
const workspace = new Workspace(options).register(BlockSchemas); | ||
const page = workspace.createPage('page0'); | ||
page.addBlockByFlavour('affine:page', { title: new page.Text('hello') }); | ||
page.addBlock('affine:page', { title: new page.Text('hello') }); | ||
page.addBlockByFlavour('affine:paragraph', { | ||
page.addBlock('affine:paragraph', { | ||
text: new page.Text( | ||
@@ -495,3 +505,3 @@ '英特尔第13代酷睿i7-1370P移动处理器现身Geekbench,14核心和5GHz' | ||
page.addBlockByFlavour('affine:paragraph', { | ||
page.addBlock('affine:paragraph', { | ||
text: new page.Text( | ||
@@ -504,6 +514,7 @@ '索尼考虑移植《GT赛车7》,又一PlayStation独占IP登陆PC平台' | ||
expect(workspace.search('处理器')).toStrictEqual(new Map([['1', id]])); | ||
expect(workspace.search('索尼')).toStrictEqual(new Map([['2', id]])); | ||
queueMicrotask(() => { | ||
expect(workspace.search('处理器')).toStrictEqual(new Map([['1', id]])); | ||
expect(workspace.search('索尼')).toStrictEqual(new Map([['2', id]])); | ||
}); | ||
}); | ||
}); |
@@ -10,3 +10,3 @@ import type { BlockModels } from '@blocksuite/global/types'; | ||
const FlavourSchema = z.string(); | ||
const TagSchema = z.object({ | ||
const ElementTagSchema = z.object({ | ||
_$litStatic$: z.string(), | ||
@@ -28,3 +28,3 @@ r: z.symbol(), | ||
flavour: FlavourSchema, | ||
tag: TagSchema, | ||
tag: ElementTagSchema, | ||
props: z | ||
@@ -116,5 +116,4 @@ .function() | ||
children: BaseBlockModel[]; | ||
// TODO use schema | ||
tags?: Y.Map<Y.Map<unknown>>; | ||
tagSchema?: Y.Map<unknown>; | ||
cells?: Y.Map<Y.Map<unknown>>; | ||
columnSchema?: Y.Map<unknown>; | ||
text?: Text; | ||
@@ -132,10 +131,10 @@ sourceId?: string; | ||
firstChild() { | ||
const children = this.children; | ||
if (!children?.length) { | ||
return null; | ||
} | ||
return children[0]; | ||
isEmpty() { | ||
return this.children.length === 0; | ||
} | ||
firstChild(): BaseBlockModel | null { | ||
return this.children[0] || null; | ||
} | ||
lastChild(): BaseBlockModel | null { | ||
@@ -142,0 +141,0 @@ if (!this.children.length) { |
@@ -10,2 +10,4 @@ /// <reference types="@blocksuite/global" /> | ||
export type { IdGenerator } from './utils/id-generator.js'; | ||
import type * as Y from 'yjs'; | ||
export type { Y }; | ||
export { | ||
@@ -12,0 +14,0 @@ createAutoIncrementIdGenerator, |
@@ -1,4 +0,4 @@ | ||
import { getDefaultPlaygroundURL } from '@blocksuite/global/utils'; | ||
import { test } from '@playwright/test'; | ||
import { defaultPlaygroundURL } from '../../../../../../tests/utils/actions'; | ||
import { collectTestResult } from '../../../__tests__/test-utils-node.js'; | ||
@@ -9,3 +9,3 @@ | ||
'/examples/blob', | ||
getDefaultPlaygroundURL(!!process.env.CI) | ||
defaultPlaygroundURL | ||
).toString(); | ||
@@ -12,0 +12,0 @@ |
@@ -13,4 +13,3 @@ import type * as Y from 'yjs'; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
Data extends Record<string, unknown> = Record<string, any>, | ||
Flags extends Record<string, unknown> = BlockSuiteFlags | ||
State extends Record<string, unknown> = Record<string, any> | ||
> { | ||
@@ -21,8 +20,12 @@ /** unprefixed id */ | ||
readonly awarenessStore: AwarenessStore; | ||
/** | ||
* @internal | ||
* @protected | ||
* @internal Used for convenient access to the underlying Yjs map, | ||
* can be used interchangeably with ySpace | ||
*/ | ||
protected readonly proxy: Data; | ||
protected readonly origin: Y.Map<Data[keyof Data]>; | ||
protected readonly _proxy: State; | ||
/** | ||
* @internal The actual underlying Yjs map | ||
*/ | ||
protected readonly _ySpace: Y.Map<State[keyof State]>; | ||
@@ -33,5 +36,5 @@ constructor(id: string, doc: BlockSuiteDoc, awarenessStore: AwarenessStore) { | ||
this.awarenessStore = awarenessStore; | ||
const targetId = this.id.startsWith('space:') ? this.id : this.prefixedId; | ||
this.origin = this.doc.getMap(targetId); | ||
this.proxy = this.doc.getMapProxy<string, Data>(targetId); | ||
const prefixedId = this.id.startsWith('space:') ? this.id : this.prefixedId; | ||
this._ySpace = this.doc.getMap(prefixedId); | ||
this._proxy = this.doc.getMapProxy<string, State>(prefixedId); | ||
} | ||
@@ -38,0 +41,0 @@ |
import { merge } from 'merge'; | ||
import { Awareness } from 'y-protocols/awareness.js'; | ||
import { AwarenessStore, RawAwarenessState } from './awareness.js'; | ||
import { AwarenessStore, type RawAwarenessState } from './awareness.js'; | ||
import type { BlobOptionsGetter } from './persistence/blob/index.js'; | ||
@@ -58,2 +58,9 @@ import type { | ||
// TODO Support ReadableStream | ||
export type InlineSuggestionProvider = (context: { | ||
title: string; | ||
text: string; | ||
abortSignal: AbortSignal; | ||
}) => string | Promise<string>; // | Promise<ReadableStream<string>>; | ||
export interface StoreOptions< | ||
@@ -81,2 +88,3 @@ Flags extends Record<string, unknown> = BlockSuiteFlags | ||
enable_block_selection_format_bar: true, | ||
enable_linked_page: false, | ||
@@ -164,12 +172,23 @@ readonly: {}, | ||
*/ | ||
exportJSX(id = '0') { | ||
exportJSX(pageId: string, blockId?: string) { | ||
const json = serializeYDoc(this.doc) as unknown as SerializedStore; | ||
if (!('space:page0' in json)) { | ||
throw new Error("Failed to convert to JSX: 'space:page0' not found"); | ||
const prefixedPageId = pageId.startsWith('space:') | ||
? pageId | ||
: `space:${pageId}`; | ||
const pageJson = json[prefixedPageId]; | ||
if (!pageJson) { | ||
throw new Error(`Page ${pageId} doesn't exist`); | ||
} | ||
if (!json['space:page0'][id]) { | ||
if (!blockId) { | ||
const pageBlockId = Object.keys(pageJson).at(0); | ||
if (!pageBlockId) { | ||
return null; | ||
} | ||
blockId = pageBlockId; | ||
} | ||
if (!pageJson[blockId]) { | ||
return null; | ||
} | ||
return yDocToJSXNode(json['space:page0'], id); | ||
return yDocToJSXNode(pageJson, blockId); | ||
} | ||
} |
@@ -16,22 +16,2 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
declare module 'yjs' { | ||
interface Text { | ||
/** | ||
* Specific addition used by @blocksuite/store | ||
* When set, we know it hasn't been applied to virgo. | ||
* When specified, we call this a "controlled operation". | ||
* | ||
* Consider renaming this to closer indicate this is simply a "controlled operation", | ||
* since we may not actually use this information. | ||
*/ | ||
meta?: | ||
| { split: true } | ||
| { join: true } | ||
| { format: true } | ||
| { delete: true } | ||
| { clear: true } | ||
| { replace: true }; | ||
} | ||
} | ||
export class Text { | ||
@@ -165,3 +145,2 @@ private _yText: Y.Text; | ||
this._yText.insert(index, content, attributes); | ||
this._yText.meta = { split: true }; | ||
}); | ||
@@ -186,3 +165,2 @@ } | ||
} | ||
this._yText.meta = { split: true }; | ||
}); | ||
@@ -200,3 +178,2 @@ } | ||
this._yText.applyDelta(delta); | ||
this._yText.meta = { join: true }; | ||
}); | ||
@@ -221,3 +198,2 @@ } | ||
this._yText.format(index, length, format); | ||
this._yText.meta = { format: true }; | ||
}); | ||
@@ -242,3 +218,2 @@ } | ||
this._yText.delete(index, length); | ||
this._yText.meta = { delete: true }; | ||
}); | ||
@@ -267,3 +242,2 @@ } | ||
this._yText.insert(index, content, attributes); | ||
this._yText.meta = { replace: true }; | ||
}); | ||
@@ -278,3 +252,2 @@ } | ||
this._yText.delete(0, this._yText.length); | ||
this._yText.meta = { clear: true }; | ||
}); | ||
@@ -281,0 +254,0 @@ } |
@@ -33,3 +33,3 @@ import * as Y from 'yjs'; | ||
const IGNORE_PROPS = [ | ||
const IGNORED_PROPS = [ | ||
'sys:id', | ||
@@ -39,4 +39,4 @@ 'sys:flavour', | ||
'prop:xywh', | ||
'meta:tags', | ||
'meta:tagSchema', | ||
'ext:cells', | ||
'ext:columnSchema', | ||
]; | ||
@@ -62,3 +62,3 @@ | ||
const props = Object.fromEntries( | ||
Object.entries(node).filter(([key]) => !IGNORE_PROPS.includes(key)) | ||
Object.entries(node).filter(([key]) => !IGNORED_PROPS.includes(key)) | ||
); | ||
@@ -65,0 +65,0 @@ |
@@ -7,3 +7,4 @@ import type { BlockModels } from '@blocksuite/global/types'; | ||
import { BaseBlockModel, BlockSchema, internalPrimitives } from '../base.js'; | ||
import type { BaseBlockModel, BlockSchema } from '../base.js'; | ||
import { internalPrimitives } from '../base.js'; | ||
import { Text } from '../text-adapter.js'; | ||
@@ -36,4 +37,4 @@ import type { Workspace } from '../workspace/index.js'; | ||
if (props.flavour === 'affine:page') { | ||
yBlock.set('meta:tags', new Y.Map()); | ||
yBlock.set('meta:tagSchema', new Y.Map()); | ||
yBlock.set('ext:cells', new Y.Map()); | ||
yBlock.set('ext:columnSchema', new Y.Map()); | ||
} | ||
@@ -40,0 +41,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { html, LitElement, PropertyValues } from 'lit'; | ||
import { html, LitElement, type PropertyValues } from 'lit'; | ||
import { customElement, property, query } from 'lit/decorators.js'; | ||
@@ -3,0 +3,0 @@ |
export { Page } from './page.js'; | ||
export type { PageMeta } from './workspace.js'; | ||
export type { PageMeta, WorkspaceOptions } from './workspace.js'; | ||
export { Workspace } from './workspace.js'; |
@@ -1,2 +0,1 @@ | ||
import type { BlockTag, TagSchema } from '@blocksuite/global/database'; | ||
import { debug } from '@blocksuite/global/debug'; | ||
@@ -10,3 +9,3 @@ import type { BlockModelProps } from '@blocksuite/global/types'; | ||
import { BaseBlockModel, internalPrimitives } from '../base.js'; | ||
import { Space, StackItem } from '../space.js'; | ||
import { Space, type StackItem } from '../space.js'; | ||
import { Text } from '../text-adapter.js'; | ||
@@ -21,2 +20,3 @@ import type { IdGenerator } from '../utils/id-generator.js'; | ||
import type { BlockSuiteDoc } from '../yjs/index.js'; | ||
import { DatabaseManager } from './database.js'; | ||
import { tryMigrate } from './migrations.js'; | ||
@@ -48,7 +48,7 @@ import type { PageMeta, Workspace } from './workspace.js'; | ||
export type PageData = { | ||
type FlatBlockMap = { | ||
[key: string]: YBlock; | ||
}; | ||
export class Page extends Space<PageData> { | ||
export class Page extends Space<FlatBlockMap> { | ||
private _workspace: Workspace; | ||
@@ -77,3 +77,9 @@ private _idGenerator: IdGenerator; | ||
}>(), | ||
subpageUpdated: new Slot<{ | ||
type: 'add' | 'delete'; | ||
id: string; | ||
subpageIds: string[]; | ||
}>(), | ||
}; | ||
readonly db: DatabaseManager; | ||
@@ -90,2 +96,3 @@ constructor( | ||
this._idGenerator = idGenerator; | ||
this.db = new DatabaseManager(this); | ||
} | ||
@@ -109,14 +116,2 @@ | ||
get tags() { | ||
assertExists(this.root); | ||
assertExists(this.root.flavour === 'affine:page'); | ||
return this.root.tags as Y.Map<Y.Map<unknown>>; | ||
} | ||
get tagSchema() { | ||
assertExists(this.root); | ||
assertExists(this.root.flavour === 'affine:page'); | ||
return this.root.tagSchema as Y.Map<unknown>; | ||
} | ||
get blobs() { | ||
@@ -128,3 +123,3 @@ return this.workspace.blobs; | ||
private get _yBlocks(): YBlocks { | ||
return this.origin; | ||
return this._ySpace; | ||
} | ||
@@ -144,3 +139,3 @@ | ||
/** @internal used for getting surface block elements for phasor */ | ||
/** @internal Used for getting surface elements for phasor. */ | ||
get ySurfaceContainer() { | ||
@@ -183,3 +178,3 @@ assertExists(this.surface); | ||
undo = () => { | ||
undo() { | ||
if (this.readonly) { | ||
@@ -190,5 +185,5 @@ console.error('cannot modify data in readonly mode'); | ||
this._history.undo(); | ||
}; | ||
} | ||
redo = () => { | ||
redo() { | ||
if (this.readonly) { | ||
@@ -199,58 +194,17 @@ console.error('cannot modify data in readonly mode'); | ||
this._history.redo(); | ||
}; | ||
} | ||
/** Capture current operations to undo stack synchronously. */ | ||
captureSync = () => { | ||
captureSync() { | ||
this._history.stopCapturing(); | ||
}; | ||
} | ||
resetHistory = () => { | ||
resetHistory() { | ||
this._history.clear(); | ||
}; | ||
updateBlockTag<Tag extends BlockTag>(id: BaseBlockModel['id'], tag: Tag) { | ||
const already = this.tags.has(id); | ||
let tags: Y.Map<unknown>; | ||
if (!already) { | ||
tags = new Y.Map(); | ||
} else { | ||
tags = this.tags.get(id) as Y.Map<unknown>; | ||
} | ||
this.transact(() => { | ||
if (!already) { | ||
this.tags.set(id, tags); | ||
} | ||
// Related issue: https://github.com/yjs/yjs/issues/255 | ||
const tagMap = new Y.Map(); | ||
tagMap.set('schemaId', tag.schemaId); | ||
tagMap.set('value', tag.value); | ||
tags.set(tag.schemaId, tagMap); | ||
}); | ||
} | ||
getBlockTagByTagSchema( | ||
model: BaseBlockModel, | ||
schema: TagSchema | ||
): BlockTag | null { | ||
const tags = this.tags.get(model.id); | ||
const tagMap = (tags?.get(schema.id) as Y.Map<unknown>) ?? null; | ||
if (!tagMap) { | ||
return null; | ||
} | ||
return { | ||
schemaId: tagMap.get('schemaId') as string, | ||
value: tagMap.get('value') as unknown, | ||
}; | ||
createId() { | ||
return this._idGenerator(); | ||
} | ||
getTagSchema(id: TagSchema['id']): TagSchema | null { | ||
return (this.tagSchema.get(id) ?? null) as TagSchema | null; | ||
} | ||
setTagSchema(schema: Omit<TagSchema, 'id'>): string { | ||
const id = this._idGenerator(); | ||
this.transact(() => this.tagSchema.set(id, { ...schema, id })); | ||
return id; | ||
} | ||
getBlockById(id: string) { | ||
@@ -356,3 +310,3 @@ return this._blockMap.get(id) ?? null; | ||
@debug('CRUD') | ||
public addBlocksByFlavour< | ||
addBlocks< | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -374,3 +328,3 @@ ALLProps extends Record<string, any> = BlockModelProps, | ||
blocks.forEach(block => { | ||
const id = this.addBlockByFlavour<ALLProps, Flavour>( | ||
const id = this.addBlock<ALLProps, Flavour>( | ||
block.flavour, | ||
@@ -389,3 +343,3 @@ block.blockProps ?? {}, | ||
@debug('CRUD') | ||
public addBlockByFlavour< | ||
addBlock< | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -440,6 +394,9 @@ ALLProps extends Record<string, any> = BlockModelProps, | ||
if (typeof parent === 'string') { | ||
parent = this._blockMap.get(parent); | ||
parent = this._blockMap.get(parent) ?? null; | ||
} | ||
const parentId = parent === null ? null : parent?.id ?? this.root?.id; | ||
let parentId = null; | ||
if (parent !== null) { | ||
parentId = parent?.id ?? this.root?.id; | ||
} | ||
@@ -452,2 +409,8 @@ if (parentId) { | ||
} | ||
if (flavour === 'affine:page') { | ||
this.workspace.setPageMeta(this.id, { | ||
title: blockProps.title?.toString(), | ||
}); | ||
} | ||
}); | ||
@@ -463,16 +426,8 @@ | ||
updateBlockById(id: string, props: Partial<BlockProps>) { | ||
if (this.readonly) { | ||
console.error('cannot modify data in readonly mode'); | ||
return; | ||
} | ||
const model = this._blockMap.get(id) as BaseBlockModel; | ||
this.updateBlock(model, props); | ||
} | ||
@debug('CRUD') | ||
moveBlocks( | ||
blocks: BaseBlockModel[], | ||
targetModel: BaseBlockModel, | ||
top = true | ||
newParent: BaseBlockModel, | ||
newSibling: BaseBlockModel | null = null, | ||
insertBeforeSibling = true | ||
) { | ||
@@ -485,27 +440,28 @@ if (this.readonly) { | ||
const firstBlock = blocks[0]; | ||
const currentParentModel = this.getParent(firstBlock); | ||
const currentParent = this.getParent(firstBlock); | ||
// the blocks must have the same parent (siblings) | ||
if (blocks.some(block => this.getParent(block) !== currentParentModel)) { | ||
if (blocks.some(block => this.getParent(block) !== currentParent)) { | ||
console.error('the blocks must have the same parent'); | ||
} | ||
const nextParentModel = this.getParent(targetModel); | ||
if (currentParentModel === null || nextParentModel === null) { | ||
throw new Error('cannot find parent model'); | ||
if (currentParent === null || newParent === null) { | ||
throw new Error("Can't find parent model"); | ||
} | ||
this.transact(() => { | ||
const yParentA = this._yBlocks.get(currentParentModel.id) as YBlock; | ||
const yParentA = this._yBlocks.get(currentParent.id) as YBlock; | ||
const yChildrenA = yParentA.get('sys:children') as Y.Array<string>; | ||
const idx = yChildrenA.toArray().findIndex(id => id === firstBlock.id); | ||
yChildrenA.delete(idx, blocks.length); | ||
const yParentB = this._yBlocks.get(nextParentModel.id) as YBlock; | ||
const yParentB = this._yBlocks.get(newParent.id) as YBlock; | ||
const yChildrenB = yParentB.get('sys:children') as Y.Array<string>; | ||
const nextIdx = yChildrenB | ||
.toArray() | ||
.findIndex(id => id === targetModel.id); | ||
let nextIdx = 0; | ||
if (newSibling) { | ||
nextIdx = yChildrenB.toArray().findIndex(id => id === newSibling.id); | ||
} | ||
const ids = blocks.map(block => block.id); | ||
if (top) { | ||
if (insertBeforeSibling) { | ||
yChildrenB.insert(nextIdx, ids); | ||
@@ -516,4 +472,4 @@ } else { | ||
}); | ||
currentParentModel.propsUpdated.emit(); | ||
nextParentModel.propsUpdated.emit(); | ||
currentParent.propsUpdated.emit(); | ||
newParent.propsUpdated.emit(); | ||
} | ||
@@ -551,2 +507,4 @@ | ||
model.propsUpdated.emit(); | ||
this.slots.blockUpdated.emit({ | ||
@@ -583,13 +541,7 @@ type: 'update', | ||
}); | ||
const ids = this.addBlocksByFlavour(blocks, parent.id, insertIndex); | ||
return ids; | ||
return this.addBlocks(blocks, parent.id, insertIndex); | ||
} else { | ||
assertExists(props[0].flavour); | ||
const { flavour, ...blockProps } = props[0]; | ||
const id = this.addBlockByFlavour( | ||
flavour, | ||
blockProps, | ||
parent.id, | ||
insertIndex | ||
); | ||
const id = this.addBlock(flavour, blockProps, parent.id, insertIndex); | ||
return [id]; | ||
@@ -599,11 +551,2 @@ } | ||
deleteBlockById(id: string) { | ||
if (this.readonly) { | ||
console.error('cannot modify data in readonly mode'); | ||
return; | ||
} | ||
const model = this._blockMap.get(id) as BaseBlockModel; | ||
this.deleteBlock(model); | ||
} | ||
@debug('CRUD') | ||
@@ -630,6 +573,8 @@ deleteBlock( | ||
} else if (options.bringChildrenTo instanceof BaseBlockModel) { | ||
options.bringChildrenTo.children.unshift(...model.children); | ||
options.bringChildrenTo.children.push(...model.children); | ||
} | ||
this._blockMap.delete(model.id); | ||
model.propsUpdated.emit(); | ||
this.transact(() => { | ||
@@ -650,3 +595,3 @@ this._yBlocks.delete(model.id); | ||
} else if (options.bringChildrenTo instanceof BaseBlockModel) { | ||
this.updateBlockById(options.bringChildrenTo.id, { | ||
this.updateBlock(options.bringChildrenTo, { | ||
children: options.bringChildrenTo.children, | ||
@@ -664,3 +609,3 @@ }); | ||
syncFromExistingDoc() { | ||
trySyncFromExistingDoc() { | ||
if (this._synced) { | ||
@@ -670,5 +615,7 @@ throw new Error('Cannot sync from existing doc more than once'); | ||
tryMigrate(this.doc); | ||
if ((this.workspace.meta.pages?.length ?? 0) <= 1) { | ||
tryMigrate(this.doc); | ||
this._handleVersion(); | ||
} | ||
this._handleVersion(); | ||
this._initYBlocks(); | ||
@@ -803,4 +750,4 @@ | ||
if (matchFlavours(model, ['affine:page'] as const)) { | ||
model.tags = yBlock.get('meta:tags') as Y.Map<Y.Map<unknown>>; | ||
model.tagSchema = yBlock.get('meta:tagSchema') as Y.Map<unknown>; | ||
model.cells = yBlock.get('ext:cells') as Y.Map<Y.Map<unknown>>; | ||
model.columnSchema = yBlock.get('ext:columnSchema') as Y.Map<unknown>; | ||
@@ -815,3 +762,3 @@ const titleText = yBlock.get('prop:title') as Y.Text; | ||
(model as any).columns = ( | ||
yBlock.get('prop:columns') as Y.Array<unknown> | ||
yBlock.get('prop:columns') as Y.Array<string> | ||
).toArray(); | ||
@@ -834,4 +781,5 @@ } | ||
const child = this._blockMap.get(id) as BaseBlockModel; | ||
model.children[index as number] = child; | ||
model.children[index as number] = this._blockMap.get( | ||
id | ||
) as BaseBlockModel; | ||
} | ||
@@ -844,2 +792,3 @@ }); | ||
this.slots.rootAdded.emit(model); | ||
this.workspace.slots.pageAdded.emit(this.id); | ||
} else if (isSurface) { | ||
@@ -969,5 +918,16 @@ this._root = [this.root as BaseBlockModel, model]; | ||
} | ||
} else if ( | ||
event.path.includes('ext:columnSchema') || | ||
event.path.includes('ext:cells') | ||
) { | ||
const blocks = this.getBlockByFlavour('affine:database'); | ||
blocks.forEach(block => { | ||
// todo: refactor here | ||
// force update all blocks that used tagSchema, which is not efficient | ||
// but it's ok for now | ||
block.propsUpdated.emit(); | ||
}); | ||
} | ||
} else { | ||
if (event.path.includes('meta:tags')) { | ||
if (event.path.includes('ext:cells')) { | ||
// todo: refactor here | ||
@@ -974,0 +934,0 @@ const blockId = event.path[2] as string; |
import type { DocumentSearchOptions } from 'flexsearch'; | ||
import FlexSearch from 'flexsearch'; | ||
import { Doc, Map as YMap, Text as YText } from 'yjs'; | ||
import type { Doc, Map as YMap } from 'yjs'; | ||
import { Text as YText } from 'yjs'; | ||
@@ -83,3 +84,3 @@ import type { YBlock } from './page.js'; | ||
onCreatePage(pageId: string) { | ||
onPageCreated(pageId: string) { | ||
this._handlePageIndexing(pageId, this._getPage(pageId)); | ||
@@ -86,0 +87,0 @@ } |
@@ -5,7 +5,8 @@ import { assertExists, Slot } from '@blocksuite/global/utils'; | ||
import { AwarenessStore, BlobUploadState } from '../awareness.js'; | ||
import type { AwarenessStore } from '../awareness.js'; | ||
import { BlobUploadState } from '../awareness.js'; | ||
import { BlockSchema, internalPrimitives } from '../base.js'; | ||
import type { BlobStorage } from '../persistence/blob/index.js'; | ||
import { | ||
BlobOptionsGetter, | ||
BlobStorage, | ||
type BlobOptionsGetter, | ||
BlobSyncState, | ||
@@ -15,7 +16,15 @@ getBlobStorage, | ||
import { Space } from '../space.js'; | ||
import { Store, StoreOptions } from '../store.js'; | ||
import { | ||
type InlineSuggestionProvider, | ||
Store, | ||
type StoreOptions, | ||
} from '../store.js'; | ||
import type { BlockSuiteDoc } from '../yjs/index.js'; | ||
import { Page } from './page.js'; | ||
import { Indexer, QueryContent } from './search.js'; | ||
import { Indexer, type QueryContent } from './search.js'; | ||
export type WorkspaceOptions = { | ||
experimentalInlineSuggestionProvider?: InlineSuggestionProvider; | ||
} & StoreOptions; | ||
export interface PageMeta { | ||
@@ -25,7 +34,8 @@ id: string; | ||
createDate: number; | ||
subpageIds: string[]; | ||
[key: string]: string | number | boolean; | ||
[key: string]: string | number | boolean | undefined | (string | number)[]; | ||
} | ||
type WorkspaceMetaFields = { | ||
type WorkspaceMetaState = { | ||
pages?: Y.Array<unknown>; | ||
@@ -37,9 +47,7 @@ versions?: Y.Map<unknown>; | ||
class WorkspaceMeta< | ||
Flags extends Record<string, unknown> = BlockSuiteFlags | ||
> extends Space<WorkspaceMetaFields, Flags> { | ||
class WorkspaceMeta extends Space<WorkspaceMetaState> { | ||
private _prevPages = new Set<string>(); | ||
pageAdded = new Slot<string>(); | ||
pageRemoved = new Slot<string>(); | ||
pagesUpdated = new Slot(); | ||
pageMetaAdded = new Slot<string>(); | ||
pageMetaRemoved = new Slot<string>(); | ||
pageMetasUpdated = new Slot(); | ||
commonFieldsUpdated = new Slot(); | ||
@@ -49,15 +57,15 @@ | ||
super(id, doc, awarenessStore); | ||
this.origin.observeDeep(this._handleEvents); | ||
this._ySpace.observeDeep(this._handleWorkspaceMetaEvents); | ||
} | ||
get pages() { | ||
return this.proxy.pages; | ||
return this._proxy.pages; | ||
} | ||
get name() { | ||
return this.proxy.name; | ||
return this._proxy.name; | ||
} | ||
get avatar() { | ||
return this.proxy.avatar; | ||
return this._proxy.avatar; | ||
} | ||
@@ -67,3 +75,3 @@ | ||
this.doc.transact(() => { | ||
this.proxy.name = name; | ||
this._proxy.name = name; | ||
}); | ||
@@ -74,3 +82,3 @@ } | ||
this.doc.transact(() => { | ||
this.proxy.avatar = avatar; | ||
this._proxy.avatar = avatar; | ||
}); | ||
@@ -80,3 +88,3 @@ } | ||
get pageMetas() { | ||
return this.proxy.pages?.toJSON() ?? ([] as PageMeta[]); | ||
return (this._proxy.pages?.toJSON() as PageMeta[]) ?? ([] as PageMeta[]); | ||
} | ||
@@ -89,8 +97,5 @@ | ||
addPageMeta(page: PageMeta, index?: number) { | ||
const yPage = new Y.Map(); | ||
this.doc.transact(() => { | ||
const pages: Y.Array<unknown> = this.pages ?? new Y.Array(); | ||
Object.entries(page).forEach(([key, value]) => { | ||
yPage.set(key, value); | ||
}); | ||
const yPage = this._transformObjectToYMap(page); | ||
if (index === undefined) { | ||
@@ -102,3 +107,3 @@ pages.push([yPage]); | ||
if (!this.pages) { | ||
this.origin.set('pages', pages); | ||
this._ySpace.set('pages', pages); | ||
} | ||
@@ -114,3 +119,3 @@ }); | ||
if (!this.pages) { | ||
this.origin.set('pages', new Y.Array()); | ||
this._ySpace.set('pages', new Y.Array()); | ||
} | ||
@@ -127,12 +132,18 @@ if (index === -1) return; | ||
removePage(id: string) { | ||
// you cannot delete a page if there's no page | ||
assertExists(this.pages); | ||
const pages = this.pages.toJSON() as PageMeta[]; | ||
const index = pages.findIndex((page: PageMeta) => id === page.id); | ||
/** Adjust the index of a page inside the pageMetss list */ | ||
shiftPageMeta(pageId: string, newIndex: number) { | ||
const pageMetas = (this.pages ?? new Y.Array()).toJSON() as PageMeta[]; | ||
const index = pageMetas.findIndex((page: PageMeta) => pageId === page.id); | ||
if (index === -1) return; | ||
const yPage = this._transformObjectToYMap(pageMetas[index]); | ||
this.doc.transact(() => { | ||
assertExists(this.pages); | ||
if (index !== -1) { | ||
this.pages.delete(index, 1); | ||
this.pages.delete(index, 1); | ||
if (newIndex > this.pages.length) { | ||
this.pages.push([yPage]); | ||
} else { | ||
this.pages.insert(newIndex, [yPage]); | ||
} | ||
@@ -142,2 +153,16 @@ }); | ||
removePageMeta(id: string) { | ||
// you cannot delete a page if there's no page | ||
assertExists(this.pages); | ||
const pageMetas = this.pages.toJSON() as PageMeta[]; | ||
const index = pageMetas.findIndex((page: PageMeta) => id === page.id); | ||
if (index === -1) { | ||
return; | ||
} | ||
this.doc.transact(() => { | ||
assertExists(this.pages); | ||
this.pages.delete(index, 1); | ||
}); | ||
} | ||
/** | ||
@@ -147,3 +172,3 @@ * @internal Only for page initialization | ||
writeVersion(workspace: Workspace) { | ||
let versions = this.proxy.versions; | ||
let versions = this._proxy.versions; | ||
if (!versions) { | ||
@@ -154,3 +179,3 @@ versions = new Y.Map<unknown>(); | ||
}); | ||
this.origin.set('versions', versions); | ||
this._ySpace.set('versions', versions); | ||
return; | ||
@@ -166,3 +191,3 @@ } else { | ||
validateVersion(workspace: Workspace) { | ||
const versions = this.proxy.versions?.toJSON(); | ||
const versions = this._proxy.versions?.toJSON(); | ||
if (!versions) { | ||
@@ -201,3 +226,3 @@ throw new Error( | ||
private _handlePageEvent() { | ||
private _handlePageMetaEvent() { | ||
const { pageMetas, _prevPages } = this; | ||
@@ -211,4 +236,3 @@ | ||
if (!_prevPages.has(pageMeta.id)) { | ||
// Ensure following YEvent handler could be triggered in correct order. | ||
queueMicrotask(() => this.pageAdded.emit(pageMeta.id)); | ||
this.pageMetaAdded.emit(pageMeta.id); | ||
} | ||
@@ -220,3 +244,3 @@ }); | ||
if (isRemoved) { | ||
this.pageRemoved.emit(prevPageId); | ||
this.pageMetaRemoved.emit(prevPageId); | ||
} | ||
@@ -228,3 +252,3 @@ }); | ||
this.pagesUpdated.emit(); | ||
this.pageMetasUpdated.emit(); | ||
} | ||
@@ -236,3 +260,3 @@ | ||
private _handleEvents = ( | ||
private _handleWorkspaceMetaEvents = ( | ||
events: Y.YEvent<Y.Array<unknown> | Y.Text | Y.Map<unknown>>[] | ||
@@ -242,3 +266,3 @@ ) => { | ||
const hasKey = (k: string) => | ||
e.target === this.origin && e.changes.keys.has(k); | ||
e.target === this._ySpace && e.changes.keys.has(k); | ||
@@ -250,3 +274,3 @@ if ( | ||
) { | ||
this._handlePageEvent(); | ||
this._handlePageMetaEvent(); | ||
} | ||
@@ -259,2 +283,10 @@ | ||
}; | ||
private _transformObjectToYMap(obj: Record<string, unknown>) { | ||
const yMap = new Y.Map(); | ||
Object.entries(obj).forEach(([key, value]) => { | ||
yMap.set(key, value); | ||
}); | ||
return yMap; | ||
} | ||
} | ||
@@ -273,6 +305,6 @@ | ||
slots: { | ||
pagesUpdated: Slot; | ||
pageAdded: Slot<string>; | ||
pageRemoved: Slot<string>; | ||
slots = { | ||
pagesUpdated: new Slot(), | ||
pageAdded: new Slot<string>(), | ||
pageRemoved: new Slot<string>(), | ||
}; | ||
@@ -283,34 +315,20 @@ | ||
constructor(options: StoreOptions) { | ||
this._store = new Store(options); | ||
readonly inlineSuggestionProvider?: InlineSuggestionProvider; | ||
constructor({ | ||
experimentalInlineSuggestionProvider, | ||
...storeOptions | ||
}: WorkspaceOptions) { | ||
this.inlineSuggestionProvider = experimentalInlineSuggestionProvider; | ||
this._store = new Store(storeOptions); | ||
this._indexer = new Indexer(this.doc); | ||
if (options.blobOptionsGetter) { | ||
this._blobOptionsGetter = options.blobOptionsGetter; | ||
if (storeOptions.blobOptionsGetter) { | ||
this._blobOptionsGetter = storeOptions.blobOptionsGetter; | ||
} | ||
if (!options.isSSR) { | ||
this._blobStorage = getBlobStorage(options.id, k => { | ||
if (!storeOptions.isSSR) { | ||
this._blobStorage = getBlobStorage(storeOptions.id, k => { | ||
return this._blobOptionsGetter ? this._blobOptionsGetter(k) : ''; | ||
}); | ||
this._blobStorage.then(blobStorage => { | ||
blobStorage?.slots.onBlobSyncStateChange.on(state => { | ||
const blobId = state.id; | ||
const syncState = state.state; | ||
if ( | ||
syncState === BlobSyncState.Waiting || | ||
syncState === BlobSyncState.Syncing | ||
) { | ||
this.awarenessStore.setBlobState(blobId, BlobUploadState.Uploading); | ||
return; | ||
} | ||
if ( | ||
syncState === BlobSyncState.Success || | ||
syncState === BlobSyncState.Failed | ||
) { | ||
this.awarenessStore.setBlobState(blobId, BlobUploadState.Uploaded); | ||
return; | ||
} | ||
}); | ||
}); | ||
this._initBlobStorage(); | ||
} else { | ||
@@ -322,10 +340,8 @@ // blob storage is not reachable in server side | ||
this.meta = new WorkspaceMeta('space:meta', this.doc, this.awarenessStore); | ||
this._bindPageMetaEvents(); | ||
this.slots = { | ||
pagesUpdated: this.meta.pagesUpdated, | ||
pageAdded: this.meta.pageAdded, | ||
pageRemoved: this.meta.pageRemoved, | ||
}; | ||
this._handlePageEvent(); | ||
this.slots.pageAdded.on(id => { | ||
// For potentially batch-added blocks, it's best to build index asynchronously | ||
queueMicrotask(() => this._indexer.onPageCreated(id)); | ||
}); | ||
} | ||
@@ -376,3 +392,3 @@ | ||
// the meta space is not included | ||
return this._store.spaces as Map<string, Page>; | ||
return this._store.spaces as Map<`space:${string}`, Page>; | ||
} | ||
@@ -384,2 +400,6 @@ | ||
get idGenerator() { | ||
return this._store.idGenerator; | ||
} | ||
register(blockSchema: z.infer<typeof BlockSchema>[]) { | ||
@@ -398,16 +418,39 @@ blockSchema.forEach(schema => { | ||
private _hasPage(pageId: string) { | ||
return this._pages.has('space:' + pageId); | ||
return this._pages.has(`space:${pageId}`); | ||
} | ||
getPage(pageId: string): Page | null { | ||
if (!pageId.startsWith('space:')) { | ||
pageId = 'space:' + pageId; | ||
} | ||
const prefixedPageId = pageId.startsWith('space:') | ||
? (pageId as `space:${string}`) | ||
: (`space:${pageId}` as const); | ||
const page = this._pages.get(pageId) ?? null; | ||
return page; | ||
return this._pages.get(prefixedPageId) ?? null; | ||
} | ||
private _handlePageEvent() { | ||
this.slots.pageAdded.on(pageId => { | ||
private _initBlobStorage() { | ||
this._blobStorage.then(blobStorage => { | ||
blobStorage?.slots.onBlobSyncStateChange.on(state => { | ||
const blobId = state.id; | ||
const syncState = state.state; | ||
if ( | ||
syncState === BlobSyncState.Waiting || | ||
syncState === BlobSyncState.Syncing | ||
) { | ||
this.awarenessStore.setBlobState(blobId, BlobUploadState.Uploading); | ||
return; | ||
} | ||
if ( | ||
syncState === BlobSyncState.Success || | ||
syncState === BlobSyncState.Failed | ||
) { | ||
this.awarenessStore.setBlobState(blobId, BlobUploadState.Uploaded); | ||
return; | ||
} | ||
}); | ||
}); | ||
} | ||
private _bindPageMetaEvents() { | ||
this.meta.pageMetaAdded.on(pageId => { | ||
const page = new Page( | ||
@@ -421,10 +464,11 @@ this, | ||
this._store.addSpace(page); | ||
page.syncFromExistingDoc(); | ||
this._indexer.onCreatePage(pageId); | ||
page.trySyncFromExistingDoc(); | ||
}); | ||
this.slots.pageRemoved.on(id => { | ||
this.meta.pageMetasUpdated.on(() => this.slots.pagesUpdated.emit()); | ||
this.meta.pageMetaRemoved.on(id => { | ||
const page = this.getPage(id) as Page; | ||
page.dispose(); | ||
this._store.removeSpace(page); | ||
this.slots.pageRemoved.emit(id); | ||
// TODO remove page from indexer | ||
@@ -434,6 +478,9 @@ }); | ||
createPage(pageId: string) { | ||
createPage(pageId: string, parentId?: string) { | ||
if (this._hasPage(pageId)) { | ||
throw new Error('page already exists'); | ||
} | ||
if (parentId && !this._hasPage(parentId)) { | ||
throw new Error('parent page not found'); | ||
} | ||
@@ -444,3 +491,24 @@ this.meta.addPageMeta({ | ||
createDate: +new Date(), | ||
subpageIds: [], | ||
}); | ||
if (parentId) { | ||
const parentPage = this.getPage(parentId) as Page; | ||
const parentPageMeta = this.meta.getPageMeta(parentId); | ||
assertExists(parentPageMeta); | ||
// Compatibility process: the old data not has `subpageIds`, it should be an empty array | ||
const subpageIds = [...(parentPageMeta.subpageIds ?? []), pageId]; | ||
this.setPageMeta(parentId, { | ||
subpageIds, | ||
}); | ||
parentPage.slots.subpageUpdated.emit({ | ||
type: 'add', | ||
id: pageId, | ||
subpageIds, | ||
}); | ||
} | ||
return this.getPage(pageId) as Page; | ||
} | ||
@@ -453,4 +521,42 @@ | ||
shiftPage(pageId: string, newIndex: number) { | ||
this.meta.shiftPageMeta(pageId, newIndex); | ||
} | ||
removePage(pageId: string) { | ||
this.meta.removePage(pageId); | ||
const pageMeta = this.meta.getPageMeta(pageId); | ||
assertExists(pageMeta); | ||
const parentId = this.meta.pageMetas.find(meta => | ||
meta.subpageIds.includes(pageId) | ||
)?.id; | ||
if (pageMeta.subpageIds?.length) { | ||
pageMeta.subpageIds.forEach((subpageId: string) => { | ||
this.removePage(subpageId); | ||
}); | ||
} | ||
if (parentId) { | ||
const parentPageMeta = this.meta.getPageMeta(parentId); | ||
assertExists(parentPageMeta); | ||
const parentPage = this.getPage(parentId) as Page; | ||
const subpageIds = parentPageMeta.subpageIds.filter( | ||
(subpageId: string) => subpageId !== pageMeta.id | ||
); | ||
this.setPageMeta(parentPage.id, { | ||
subpageIds, | ||
}); | ||
parentPage.slots.subpageUpdated.emit({ | ||
type: 'delete', | ||
id: pageId, | ||
subpageIds, | ||
}); | ||
} | ||
const page = this.getPage(pageId); | ||
if (!page) return; | ||
page.dispose(); | ||
this.meta.removePageMeta(pageId); | ||
this._store.removeSpace(page); | ||
} | ||
@@ -485,5 +591,6 @@ | ||
*/ | ||
exportJSX(id = '0') { | ||
return this._store.exportJSX(id); | ||
exportJSX(blockId?: string, pageId = this.meta.pageMetas.at(0)?.id) { | ||
assertExists(pageId); | ||
return this._store.exportJSX(pageId, blockId); | ||
} | ||
} |
@@ -5,3 +5,3 @@ import { debug } from '@blocksuite/global/debug'; | ||
import { createYMapProxy, ProxyConfig } from './proxy.js'; | ||
import { createYMapProxy, type ProxyConfig } from './proxy.js'; | ||
@@ -31,5 +31,5 @@ export type BlockSuiteDocAllowedValue = | ||
@debug('transact') | ||
transact(f: (arg0: Transaction) => void, origin?: number) { | ||
super.transact(f, origin); | ||
transact<T>(f: (arg0: Transaction) => T, origin?: number) { | ||
return super.transact(f, origin); | ||
} | ||
} |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
169132
45
4741
- Removed@blocksuite/global@0.5.0-alpha.1(transitive)
- Removed@blocksuite/virgo@0.5.0-alpha.1(transitive)
- Removed@lit-labs/ssr-dom-shim@1.3.0(transitive)
- Removed@lit/reactive-element@1.6.3(transitive)
- Removed@types/trusted-types@2.0.7(transitive)
- Removedansi-colors@4.1.3(transitive)
- Removedlit@2.8.0(transitive)
- Removedlit-element@3.3.3(transitive)
- Removedlit-html@2.8.0(transitive)
Updatedky@^0.33.3
Updatedlib0@^0.2.68
Updatedy-webrtc@^10.2.5
Updatedzod@^3.21.4