feat: seperate createDoc and createStore (#11182)

This commit is contained in:
Saul-Mirone
2025-03-26 11:03:47 +00:00
parent d6093e1d66
commit 0a8d8e0a6b
70 changed files with 337 additions and 312 deletions

View File

@@ -36,15 +36,16 @@ describe('editor host', () => {
const collection = new TestWorkspace(createTestOptions());
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
const store = doc.getStore({ extensions });
doc.load();
const rootId = doc.addBlock('test:page');
const noteId = doc.addBlock('test:note', {}, rootId);
const headingId = doc.addBlock('test:heading', { type: 'h1' }, noteId);
const headingBlock = doc.getBlock(headingId)!;
const rootId = store.addBlock('test:page');
const noteId = store.addBlock('test:note', {}, rootId);
const headingId = store.addBlock('test:heading', { type: 'h1' }, noteId);
const headingBlock = store.getBlock(headingId)!;
const editorContainer = new TestEditorContainer();
editorContainer.doc = doc;
editorContainer.doc = store;
editorContainer.specs = testSpecs;
document.body.append(editorContainer);

View File

@@ -80,16 +80,16 @@ function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({
id: docId,
const doc = collection.createDoc(docId);
doc.load();
const store = doc.getStore({
extensions: [
pageSchemaExtension,
tableSchemaExtension,
flatTableSchemaExtension,
],
});
doc.load();
return doc;
return store;
}
test('init block without props should add default props', () => {

View File

@@ -61,12 +61,12 @@ function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({
id: docId,
const doc = collection.createDoc(docId);
const store = doc.getStore({
extensions,
});
doc.load();
return doc;
return store;
}
function requestIdleCallbackPolyfill(
@@ -97,7 +97,7 @@ describe('basic', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'doc:home' });
const doc = collection.createDoc('doc:home');
doc.load();
const actual = serializCollection(collection.doc);
const actualDoc = actual[spaceMetaId].pages[0] as DocMeta;
@@ -148,23 +148,23 @@ describe('basic', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({
id: 'space:0',
const doc = collection.createDoc('space:0');
const store = doc.getStore({
extensions,
});
const readyCallback = vi.fn();
const rootAddedCallback = vi.fn();
doc.slots.ready.subscribe(readyCallback);
doc.slots.rootAdded.subscribe(rootAddedCallback);
store.slots.ready.subscribe(readyCallback);
store.slots.rootAdded.subscribe(rootAddedCallback);
doc.load(() => {
const rootId = doc.addBlock('affine:page', {
store.load(() => {
const rootId = store.addBlock('affine:page', {
title: new Text(),
});
expect(rootAddedCallback).toBeCalledTimes(1);
doc.addBlock('affine:note', {}, rootId);
store.addBlock('affine:note', {}, rootId);
});
expect(readyCallback).toBeCalledTimes(1);
@@ -175,12 +175,12 @@ describe('basic', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const collection2 = new TestWorkspace(options);
const doc = collection.createDoc({
id: 'space:0',
const doc = collection.createDoc('space:0');
const store = doc.getStore({
extensions,
});
doc.load(() => {
doc.addBlock('affine:page', {
store.addBlock('affine:page', {
title: new Text(),
});
});
@@ -206,9 +206,7 @@ describe('basic', () => {
// apply doc update
const update = encodeStateAsUpdate(doc.spaceDoc);
expect(collection2.docs.size).toBe(1);
const doc2 = collection2.getDoc('space:0', {
extensions,
});
const doc2 = collection2.getDoc('space:0');
if (!doc2) {
throw new Error('doc2 is not found');
}
@@ -382,12 +380,15 @@ describe('addBlock', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc0 = collection.createDoc({ id: 'doc:home' });
const doc1 = collection.createDoc({ id: 'space:doc1' });
const doc0 = collection.createDoc('doc:home');
const doc1 = collection.createDoc('space:doc1');
await Promise.all([doc0.load(), doc1.load()]);
assert.equal(collection.docs.size, 2);
const store0 = doc0.getStore({
extensions,
});
doc0.addBlock('affine:page', {
store0.addBlock('affine:page', {
title: new Text(),
});
collection.removeDoc(doc0.id);
@@ -407,8 +408,7 @@ describe('addBlock', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc0 = collection.createDoc({ id: 'doc:home' });
const doc0 = collection.createDoc('doc:home');
collection.removeDoc(doc0.id);
assert.equal(collection.docs.size, 0);
});
@@ -417,7 +417,7 @@ describe('addBlock', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
collection.createDoc({ id: 'doc:home' });
collection.createDoc('doc:home');
assert.deepEqual(
collection.meta.docMetas.map(({ id, title }) => ({

View File

@@ -31,12 +31,15 @@ test('trigger props updated', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
doc.load();
const store = doc.getStore({
extensions,
});
doc.addBlock('affine:page');
store.addBlock('affine:page');
const rootModel = doc.root as RootBlockModel;
const rootModel = store.root as RootBlockModel;
expect(rootModel).not.toBeNull();
@@ -91,12 +94,15 @@ test('stash and pop', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
doc.load();
const store = doc.getStore({
extensions,
});
doc.addBlock('affine:page');
store.addBlock('affine:page');
const rootModel = doc.root as RootBlockModel;
const rootModel = store.root as RootBlockModel;
expect(rootModel).not.toBeNull();
@@ -161,12 +167,15 @@ test('always get latest value in onChange', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
doc.load();
const store = doc.getStore({
extensions,
});
doc.addBlock('affine:page');
store.addBlock('affine:page');
const rootModel = doc.root as RootBlockModel;
const rootModel = store.root as RootBlockModel;
expect(rootModel).not.toBeNull();
@@ -207,11 +216,15 @@ test('query', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc1 = collection.createDoc({ id: 'home', extensions });
doc1.load();
const doc2 = collection.getDoc('home', { extensions });
const doc3 = collection.getDoc('home', {
const doc = collection.createDoc('home');
doc.load();
const store1 = doc.getStore({
extensions,
});
const store2 = doc.getStore({
extensions,
});
const store3 = doc.getStore({
extensions,
query: {
mode: 'loose',
@@ -223,48 +236,53 @@ test('query', () => {
],
},
});
expect(doc1).toBe(doc2);
expect(doc1).not.toBe(doc3);
expect(store1).toBe(store2);
expect(store1).not.toBe(store3);
const page = doc1.addBlock('affine:page');
const note = doc1.addBlock('affine:note', {}, page);
const paragraph1 = doc1.addBlock('affine:paragraph', {}, note);
const list1 = doc1.addBlock('affine:list' as never, {}, note);
const page = store1.addBlock('affine:page');
const note = store1.addBlock('affine:note', {}, page);
const paragraph1 = store1.addBlock('affine:paragraph', {}, note);
const list1 = store1.addBlock('affine:list' as never, {}, note);
expect(doc2?.getBlock(paragraph1)?.blockViewType).toBe('display');
expect(doc2?.getBlock(list1)?.blockViewType).toBe('display');
expect(doc3?.getBlock(list1)?.blockViewType).toBe('hidden');
expect(store2?.getBlock(paragraph1)?.blockViewType).toBe('display');
expect(store2?.getBlock(list1)?.blockViewType).toBe('display');
expect(store3?.getBlock(list1)?.blockViewType).toBe('hidden');
const list2 = doc1.addBlock('affine:list' as never, {}, note);
const list2 = store1.addBlock('affine:list' as never, {}, note);
expect(doc2?.getBlock(list2)?.blockViewType).toBe('display');
expect(doc3?.getBlock(list2)?.blockViewType).toBe('hidden');
expect(store2?.getBlock(list2)?.blockViewType).toBe('display');
expect(store3?.getBlock(list2)?.blockViewType).toBe('hidden');
});
test('local readonly', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc1 = collection.createDoc({ id: 'home', extensions });
doc1.load();
const doc2 = collection.getDoc('home', { readonly: true, extensions });
const doc3 = collection.getDoc('home', { readonly: false, extensions });
const doc = collection.createDoc('home');
const store1 = doc.getStore({
extensions,
});
const store2 = doc.getStore({
readonly: true,
extensions,
});
const store3 = doc.getStore({ readonly: false, extensions });
expect(doc1.readonly).toBeFalsy();
expect(doc2?.readonly).toBeTruthy();
expect(doc3?.readonly).toBeFalsy();
expect(store1.readonly).toBeFalsy();
expect(store2.readonly).toBeTruthy();
expect(store3.readonly).toBeFalsy();
doc1.readonly = true;
store1.readonly = true;
expect(doc1.readonly).toBeTruthy();
expect(doc2?.readonly).toBeTruthy();
expect(doc3?.readonly).toBeFalsy();
expect(store1.readonly).toBeTruthy();
expect(store2.readonly).toBeTruthy();
expect(store3.readonly).toBeFalsy();
doc1.readonly = false;
store1.readonly = false;
expect(doc1.readonly).toBeFalsy();
expect(doc2?.readonly).toBeTruthy();
expect(doc3?.readonly).toBeFalsy();
expect(store1.readonly).toBeFalsy();
expect(store2.readonly).toBeTruthy();
expect(store3.readonly).toBeFalsy();
});
describe('move blocks', () => {
@@ -274,21 +292,22 @@ describe('move blocks', () => {
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
doc.load();
const pageId = doc.addBlock('affine:page');
const page = doc.getBlock(pageId)!.model;
const store = doc.getStore({ extensions });
const pageId = store.addBlock('affine:page');
const page = store.getBlock(pageId)!.model;
const noteIds = doc.addBlocks(
const noteIds = store.addBlocks(
[1, 2, 3].map(i => ({
flavour: 'affine:note',
blockProps: { id: `${i}` },
})),
page
);
const notes = noteIds.map(id => doc.getBlock(id)!.model);
const notes = noteIds.map(id => store.getBlock(id)!.model);
context.doc = doc;
context.doc = store;
context.page = page;
context.notes = notes;
});

View File

@@ -99,9 +99,10 @@ function createTestDoc(docId = defaultDocId) {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: docId, extensions });
const doc = collection.createDoc(docId);
doc.load();
return doc;
const store = doc.getStore({ extensions });
return store;
}
describe('schema', () => {

View File

@@ -59,10 +59,11 @@ test('model to snapshot', () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
const store = doc.getStore({ extensions });
doc.load();
doc.addBlock('page');
const rootModel = doc.root as RootBlockModel;
store.addBlock('page');
const rootModel = store.root as RootBlockModel;
expect(rootModel).not.toBeNull();
const snapshot = transformer.toSnapshot({
@@ -76,10 +77,11 @@ test('snapshot to model', async () => {
const options = createTestOptions();
const collection = new TestWorkspace(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'home', extensions });
const doc = collection.createDoc('home');
const store = doc.getStore({ extensions });
doc.load();
doc.addBlock('page');
const rootModel = doc.root as RootBlockModel;
store.addBlock('page');
const rootModel = store.root as RootBlockModel;
const tempDoc = new Y.Doc();
const map = tempDoc.getMap('temp');

View File

@@ -1127,8 +1127,9 @@ export class Store {
schema: this.schema,
blobCRUD: this.workspace.blobSync,
docCRUD: {
create: (id: string) => this.workspace.createDoc({ id }),
get: (id: string) => this.workspace.getDoc(id),
create: (id: string) => this.workspace.createDoc(id).getStore({ id }),
get: (id: string) =>
this.workspace.getDoc(id)?.getStore({ id }) ?? null,
delete: (id: string) => this.workspace.removeDoc(id),
},
middlewares,

View File

@@ -4,8 +4,7 @@ import type { Awareness } from 'y-protocols/awareness.js';
import type * as Y from 'yjs';
import type { IdGenerator } from '../utils/id-generator.js';
import type { CreateBlocksOptions, Doc, GetBlocksOptions } from './doc.js';
import type { Store } from './store/store.js';
import type { Doc } from './doc.js';
import type { WorkspaceMeta } from './workspace-meta.js';
export interface Workspace {
@@ -25,8 +24,8 @@ export interface Workspace {
docRemoved: Subject<string>;
};
createDoc(options?: CreateBlocksOptions): Store;
getDoc(docId: string, options?: GetBlocksOptions): Store | null;
createDoc(docId?: string): Doc;
getDoc(docId: string): Doc | null;
removeDoc(docId: string): void;
dispose(): void;

View File

@@ -15,13 +15,7 @@ import { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs';
import type { ExtensionType } from '../extension/extension.js';
import type {
CreateBlocksOptions,
GetBlocksOptions,
Store,
Workspace,
WorkspaceMeta,
} from '../model/index.js';
import type { Doc, Workspace, WorkspaceMeta } from '../model/index.js';
import { type IdGenerator, nanoid } from '../utils/id-generator.js';
import { AwarenessStore } from '../yjs/index.js';
import { TestDoc } from './test-doc.js';
@@ -155,14 +149,9 @@ export class TestWorkspace implements Workspace {
* If the `init` parameter is passed, a `surface`, `note`, and `paragraph` block
* will be created in the doc simultaneously.
*/
createDoc(options: CreateBlocksOptions = {}) {
const {
id: docId = this.idGenerator(),
query,
readonly,
extensions,
} = options;
if (this._hasDoc(docId)) {
createDoc(docId?: string): Doc {
const id = docId ?? this.idGenerator();
if (this._hasDoc(id)) {
throw new BlockSuiteError(
ErrorCode.DocCollectionError,
'doc already exists'
@@ -170,18 +159,13 @@ export class TestWorkspace implements Workspace {
}
this.meta.addDocMeta({
id: docId,
id,
title: '',
createDate: Date.now(),
tags: [],
});
this.slots.docCreated.next(docId);
return this.getDoc(docId, {
id: docId,
query,
readonly,
extensions,
}) as Store;
this.slots.docCreated.next(id);
return this.getDoc(id) as Doc;
}
dispose() {
@@ -203,12 +187,9 @@ export class TestWorkspace implements Workspace {
return space ?? null;
}
getDoc(
docId: string,
options: GetBlocksOptions = { id: docId }
): Store | null {
getDoc(docId: string): Doc | null {
const collection = this.getBlockCollection(docId);
return collection?.getStore(options) ?? null;
return collection;
}
removeDoc(docId: string) {

View File

@@ -1,6 +1,6 @@
import { z } from 'zod';
import type { Store } from '../model/store/store';
import type { Store } from '../model';
import type { DocMeta, DocsPropertiesMeta } from '../model/workspace-meta';
export type BlockSnapshot = {