mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
104
blocksuite/framework/store/src/transformer/assets.ts
Normal file
104
blocksuite/framework/store/src/transformer/assets.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
|
||||
interface BlobCRUD {
|
||||
get: (key: string) => Promise<Blob | null> | Blob | null;
|
||||
set: (key: string, value: Blob) => Promise<string> | string;
|
||||
delete: (key: string) => Promise<void> | void;
|
||||
list: () => Promise<string[]> | string[];
|
||||
}
|
||||
|
||||
type AssetsManagerConfig = {
|
||||
blob: BlobCRUD;
|
||||
};
|
||||
|
||||
function makeNewNameWhenConflict(names: Set<string>, name: string) {
|
||||
let i = 1;
|
||||
const ext = name.split('.').at(-1) ?? '';
|
||||
let newName = name.replace(new RegExp(`.${ext}$`), ` (${i}).${ext}`);
|
||||
while (names.has(newName)) {
|
||||
newName = name.replace(new RegExp(`.${ext}$`), ` (${i}).${ext}`);
|
||||
i++;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
export class AssetsManager {
|
||||
private readonly _assetsMap = new Map<string, Blob>();
|
||||
|
||||
private readonly _blob: BlobCRUD;
|
||||
|
||||
private readonly _names = new Set<string>();
|
||||
|
||||
private readonly _pathBlobIdMap = new Map<string, string>();
|
||||
|
||||
constructor(options: AssetsManagerConfig) {
|
||||
this._blob = options.blob;
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this._assetsMap.clear();
|
||||
this._names.clear();
|
||||
}
|
||||
|
||||
getAssets() {
|
||||
return this._assetsMap;
|
||||
}
|
||||
|
||||
getPathBlobIdMap() {
|
||||
return this._pathBlobIdMap;
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this._assetsMap.size === 0;
|
||||
}
|
||||
|
||||
async readFromBlob(blobId: string) {
|
||||
if (this._assetsMap.has(blobId)) return;
|
||||
const blob = await this._blob.get(blobId);
|
||||
if (!blob) {
|
||||
console.error(`Blob ${blobId} not found in blob manager`);
|
||||
return;
|
||||
}
|
||||
if (blob instanceof File) {
|
||||
let file = blob;
|
||||
if (this._names.has(blob.name)) {
|
||||
const newName = makeNewNameWhenConflict(this._names, blob.name);
|
||||
file = new File([blob], newName, { type: blob.type });
|
||||
}
|
||||
this._assetsMap.set(blobId, file);
|
||||
this._names.add(file.name);
|
||||
return;
|
||||
}
|
||||
if (blob.type && blob.type !== 'application/octet-stream') {
|
||||
this._assetsMap.set(blobId, blob);
|
||||
return;
|
||||
}
|
||||
// Guess the file type from the buffer
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const FileType = await import('file-type');
|
||||
const fileType = await FileType.fileTypeFromBuffer(buffer);
|
||||
if (fileType) {
|
||||
const file = new File([blob], '', { type: fileType.mime });
|
||||
this._assetsMap.set(blobId, file);
|
||||
return;
|
||||
}
|
||||
this._assetsMap.set(blobId, blob);
|
||||
}
|
||||
|
||||
async writeToBlob(blobId: string) {
|
||||
const blob = this._assetsMap.get(blobId);
|
||||
if (!blob) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
`Blob ${blobId} not found in assets manager`
|
||||
);
|
||||
}
|
||||
|
||||
const exists = (await this._blob.get(blobId)) !== null;
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._blob.set(blobId, blob);
|
||||
}
|
||||
}
|
||||
78
blocksuite/framework/store/src/transformer/base.ts
Normal file
78
blocksuite/framework/store/src/transformer/base.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { BlockModel, InternalPrimitives } from '../schema/index.js';
|
||||
import { internalPrimitives } from '../schema/index.js';
|
||||
import type { AssetsManager } from './assets.js';
|
||||
import type { DraftModel } from './draft.js';
|
||||
import { fromJSON, toJSON } from './json.js';
|
||||
import type { BlockSnapshot } from './type.js';
|
||||
|
||||
type BlockSnapshotLeaf = Pick<
|
||||
BlockSnapshot,
|
||||
'id' | 'flavour' | 'props' | 'version'
|
||||
>;
|
||||
|
||||
export type FromSnapshotPayload = {
|
||||
json: BlockSnapshotLeaf;
|
||||
assets: AssetsManager;
|
||||
children: BlockSnapshot[];
|
||||
};
|
||||
|
||||
export type ToSnapshotPayload<Props extends object> = {
|
||||
model: DraftModel<BlockModel<Props>>;
|
||||
assets: AssetsManager;
|
||||
};
|
||||
|
||||
export type SnapshotNode<Props extends object> = {
|
||||
id: string;
|
||||
flavour: string;
|
||||
version: number;
|
||||
props: Props;
|
||||
};
|
||||
|
||||
export class BaseBlockTransformer<Props extends object = object> {
|
||||
protected _internal: InternalPrimitives = internalPrimitives;
|
||||
|
||||
protected _propsFromSnapshot(propsJson: Record<string, unknown>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(propsJson).map(([key, value]) => {
|
||||
return [key, fromJSON(value)];
|
||||
})
|
||||
) as Props;
|
||||
}
|
||||
|
||||
protected _propsToSnapshot(model: DraftModel) {
|
||||
return Object.fromEntries(
|
||||
model.keys.map(key => {
|
||||
const value = model[key as keyof typeof model];
|
||||
return [key, toJSON(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fromSnapshot({
|
||||
json,
|
||||
}: FromSnapshotPayload): Promise<SnapshotNode<Props>> | SnapshotNode<Props> {
|
||||
const { flavour, id, version, props: _props } = json;
|
||||
|
||||
const props = this._propsFromSnapshot(_props);
|
||||
|
||||
return {
|
||||
id,
|
||||
flavour,
|
||||
version: version ?? -1,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
toSnapshot({ model }: ToSnapshotPayload<Props>): BlockSnapshotLeaf {
|
||||
const { id, flavour, version } = model;
|
||||
|
||||
const props = this._propsToSnapshot(model);
|
||||
|
||||
return {
|
||||
id,
|
||||
flavour,
|
||||
version,
|
||||
props,
|
||||
};
|
||||
}
|
||||
}
|
||||
35
blocksuite/framework/store/src/transformer/draft.ts
Normal file
35
blocksuite/framework/store/src/transformer/draft.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { BlockModel } from '../schema/base.js';
|
||||
|
||||
type PropsInDraft = 'version' | 'flavour' | 'role' | 'id' | 'keys' | 'text';
|
||||
|
||||
type ModelProps<Model> = Model extends BlockModel<infer U> ? U : never;
|
||||
|
||||
export type DraftModel<Model extends BlockModel = BlockModel> = Pick<
|
||||
Model,
|
||||
PropsInDraft
|
||||
> & {
|
||||
children: DraftModel[];
|
||||
} & ModelProps<Model>;
|
||||
|
||||
export function toDraftModel<Model extends BlockModel = BlockModel>(
|
||||
origin: Model
|
||||
): DraftModel<Model> {
|
||||
const { id, version, flavour, role, keys, text, children } = origin;
|
||||
const props = origin.keys.reduce((acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: origin[key as keyof Model],
|
||||
};
|
||||
}, {} as ModelProps<Model>);
|
||||
|
||||
return {
|
||||
id,
|
||||
version,
|
||||
flavour,
|
||||
role,
|
||||
keys,
|
||||
text,
|
||||
children: children.map(toDraftModel),
|
||||
...props,
|
||||
} as DraftModel<Model>;
|
||||
}
|
||||
8
blocksuite/framework/store/src/transformer/index.ts
Normal file
8
blocksuite/framework/store/src/transformer/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './assets.js';
|
||||
export * from './base.js';
|
||||
export * from './draft.js';
|
||||
export * from './job.js';
|
||||
export * from './json.js';
|
||||
export * from './middleware.js';
|
||||
export * from './slice.js';
|
||||
export * from './type.js';
|
||||
630
blocksuite/framework/store/src/transformer/job.ts
Normal file
630
blocksuite/framework/store/src/transformer/job.ts
Normal file
@@ -0,0 +1,630 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { nextTick, Slot } from '@blocksuite/global/utils';
|
||||
|
||||
import type { BlockModel, BlockSchemaType } from '../schema/index.js';
|
||||
import type { Doc, DocCollection, DocMeta } from '../store/index.js';
|
||||
import { AssetsManager } from './assets.js';
|
||||
import { BaseBlockTransformer } from './base.js';
|
||||
import type { DraftModel } from './draft.js';
|
||||
import type {
|
||||
BeforeExportPayload,
|
||||
BeforeImportPayload,
|
||||
FinalPayload,
|
||||
JobMiddleware,
|
||||
JobSlots,
|
||||
} from './middleware.js';
|
||||
import { Slice } from './slice.js';
|
||||
import type {
|
||||
BlockSnapshot,
|
||||
CollectionInfoSnapshot,
|
||||
DocSnapshot,
|
||||
SliceSnapshot,
|
||||
} from './type.js';
|
||||
import {
|
||||
BlockSnapshotSchema,
|
||||
CollectionInfoSnapshotSchema,
|
||||
DocSnapshotSchema,
|
||||
SliceSnapshotSchema,
|
||||
} from './type.js';
|
||||
|
||||
export type JobConfig = {
|
||||
collection: DocCollection;
|
||||
middlewares?: JobMiddleware[];
|
||||
};
|
||||
|
||||
interface FlatSnapshot {
|
||||
snapshot: BlockSnapshot;
|
||||
parentId?: string;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
interface DraftBlockTreeNode {
|
||||
draft: DraftModel;
|
||||
snapshot: BlockSnapshot;
|
||||
children: Array<DraftBlockTreeNode>;
|
||||
}
|
||||
|
||||
// The number of blocks to insert in one batch
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export class Job {
|
||||
private readonly _adapterConfigs = new Map<string, string>();
|
||||
|
||||
private readonly _assetsManager: AssetsManager;
|
||||
|
||||
private readonly _collection: DocCollection;
|
||||
|
||||
private readonly _slots: JobSlots = {
|
||||
beforeImport: new Slot<BeforeImportPayload>(),
|
||||
afterImport: new Slot<FinalPayload>(),
|
||||
beforeExport: new Slot<BeforeExportPayload>(),
|
||||
afterExport: new Slot<FinalPayload>(),
|
||||
};
|
||||
|
||||
blockToSnapshot = (model: DraftModel): BlockSnapshot | undefined => {
|
||||
try {
|
||||
const snapshot = this._blockToSnapshot(model);
|
||||
BlockSnapshotSchema.parse(snapshot);
|
||||
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming block to snapshot:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
collectionInfoToSnapshot = (): CollectionInfoSnapshot | undefined => {
|
||||
try {
|
||||
this._slots.beforeExport.emit({
|
||||
type: 'info',
|
||||
});
|
||||
const collectionMeta = this._getCollectionMeta();
|
||||
const snapshot: CollectionInfoSnapshot = {
|
||||
type: 'info',
|
||||
id: this._collection.id,
|
||||
...collectionMeta,
|
||||
};
|
||||
this._slots.afterExport.emit({
|
||||
type: 'info',
|
||||
snapshot,
|
||||
});
|
||||
CollectionInfoSnapshotSchema.parse(snapshot);
|
||||
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming collection info to snapshot:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
docToSnapshot = (doc: Doc): DocSnapshot | undefined => {
|
||||
try {
|
||||
this._slots.beforeExport.emit({
|
||||
type: 'page',
|
||||
page: doc,
|
||||
});
|
||||
const rootModel = doc.root;
|
||||
const meta = this._exportDocMeta(doc);
|
||||
if (!rootModel) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
'Root block not found in doc'
|
||||
);
|
||||
}
|
||||
const blocks = this.blockToSnapshot(rootModel);
|
||||
if (!blocks) {
|
||||
return;
|
||||
}
|
||||
const docSnapshot: DocSnapshot = {
|
||||
type: 'page',
|
||||
meta,
|
||||
blocks,
|
||||
};
|
||||
this._slots.afterExport.emit({
|
||||
type: 'page',
|
||||
page: doc,
|
||||
snapshot: docSnapshot,
|
||||
});
|
||||
DocSnapshotSchema.parse(docSnapshot);
|
||||
|
||||
return docSnapshot;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming doc to snapshot:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
sliceToSnapshot = (slice: Slice): SliceSnapshot | undefined => {
|
||||
try {
|
||||
this._slots.beforeExport.emit({
|
||||
type: 'slice',
|
||||
slice,
|
||||
});
|
||||
const { content, pageId, workspaceId } = slice.data;
|
||||
const contentSnapshot = [];
|
||||
for (const block of content) {
|
||||
const blockSnapshot = this.blockToSnapshot(block);
|
||||
if (!blockSnapshot) {
|
||||
return;
|
||||
}
|
||||
contentSnapshot.push(blockSnapshot);
|
||||
}
|
||||
const snapshot: SliceSnapshot = {
|
||||
type: 'slice',
|
||||
workspaceId,
|
||||
pageId,
|
||||
content: contentSnapshot,
|
||||
};
|
||||
this._slots.afterExport.emit({
|
||||
type: 'slice',
|
||||
slice,
|
||||
snapshot,
|
||||
});
|
||||
SliceSnapshotSchema.parse(snapshot);
|
||||
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming slice to snapshot:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
snapshotToBlock = async (
|
||||
snapshot: BlockSnapshot,
|
||||
doc: Doc,
|
||||
parent?: string,
|
||||
index?: number
|
||||
): Promise<BlockModel | undefined> => {
|
||||
try {
|
||||
BlockSnapshotSchema.parse(snapshot);
|
||||
const model = await this._snapshotToBlock(snapshot, doc, parent, index);
|
||||
if (!model) return;
|
||||
return model;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming snapshot to block:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
snapshotToDoc = async (snapshot: DocSnapshot): Promise<Doc | undefined> => {
|
||||
try {
|
||||
this._slots.beforeImport.emit({
|
||||
type: 'page',
|
||||
snapshot,
|
||||
});
|
||||
DocSnapshotSchema.parse(snapshot);
|
||||
const { meta, blocks } = snapshot;
|
||||
const doc = this._collection.createDoc({ id: meta.id });
|
||||
doc.load();
|
||||
await this.snapshotToBlock(blocks, doc);
|
||||
this._slots.afterImport.emit({
|
||||
type: 'page',
|
||||
snapshot,
|
||||
page: doc,
|
||||
});
|
||||
|
||||
return doc;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming snapshot to doc:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
snapshotToModelData = async (snapshot: BlockSnapshot) => {
|
||||
try {
|
||||
const { children, flavour, props, id } = snapshot;
|
||||
|
||||
const schema = this._getSchema(flavour);
|
||||
const snapshotLeaf = {
|
||||
id,
|
||||
flavour,
|
||||
props,
|
||||
};
|
||||
const transformer = this._getTransformer(schema);
|
||||
const modelData = await transformer.fromSnapshot({
|
||||
json: snapshotLeaf,
|
||||
assets: this._assetsManager,
|
||||
children,
|
||||
});
|
||||
|
||||
return modelData;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming snapshot to model data:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
snapshotToSlice = async (
|
||||
snapshot: SliceSnapshot,
|
||||
doc: Doc,
|
||||
parent?: string,
|
||||
index?: number
|
||||
): Promise<Slice | undefined> => {
|
||||
SliceSnapshotSchema.parse(snapshot);
|
||||
try {
|
||||
this._slots.beforeImport.emit({
|
||||
type: 'slice',
|
||||
snapshot,
|
||||
});
|
||||
|
||||
const { content, workspaceId, pageId } = snapshot;
|
||||
|
||||
// Create a temporary root snapshot to encompass all content blocks
|
||||
const tmpRootSnapshot: BlockSnapshot = {
|
||||
id: 'temporary-root',
|
||||
flavour: 'affine:page',
|
||||
props: {},
|
||||
type: 'block',
|
||||
children: content,
|
||||
};
|
||||
|
||||
for (const block of content) {
|
||||
this._triggerBeforeImportEvent(block, parent, index);
|
||||
}
|
||||
const flatSnapshots: FlatSnapshot[] = [];
|
||||
this._flattenSnapshot(tmpRootSnapshot, flatSnapshots, parent, index);
|
||||
|
||||
const blockTree = await this._convertFlatSnapshots(flatSnapshots);
|
||||
|
||||
await this._insertBlockTree(blockTree.children, doc, parent, index);
|
||||
|
||||
const contentBlocks = blockTree.children
|
||||
.map(tree => doc.getBlockById(tree.draft.id))
|
||||
.filter(Boolean) as DraftModel[];
|
||||
|
||||
const slice = new Slice({
|
||||
content: contentBlocks,
|
||||
workspaceId,
|
||||
pageId,
|
||||
});
|
||||
|
||||
this._slots.afterImport.emit({
|
||||
type: 'slice',
|
||||
snapshot,
|
||||
slice,
|
||||
});
|
||||
|
||||
return slice;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming snapshot to slice:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
walk = (snapshot: DocSnapshot, callback: (block: BlockSnapshot) => void) => {
|
||||
const walk = (block: BlockSnapshot) => {
|
||||
try {
|
||||
callback(block);
|
||||
} catch (error) {
|
||||
console.error(`Error when walking snapshot:`);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (block.children) {
|
||||
block.children.forEach(walk);
|
||||
}
|
||||
};
|
||||
|
||||
walk(snapshot.blocks);
|
||||
};
|
||||
|
||||
get adapterConfigs() {
|
||||
return this._adapterConfigs;
|
||||
}
|
||||
|
||||
get assets() {
|
||||
return this._assetsManager.getAssets();
|
||||
}
|
||||
|
||||
get assetsManager() {
|
||||
return this._assetsManager;
|
||||
}
|
||||
|
||||
get collection() {
|
||||
return this._collection;
|
||||
}
|
||||
|
||||
constructor({ collection, middlewares = [] }: JobConfig) {
|
||||
this._collection = collection;
|
||||
this._assetsManager = new AssetsManager({ blob: collection.blobSync });
|
||||
|
||||
middlewares.forEach(middleware => {
|
||||
middleware({
|
||||
slots: this._slots,
|
||||
assetsManager: this._assetsManager,
|
||||
collection: this._collection,
|
||||
adapterConfigs: this._adapterConfigs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _blockToSnapshot(model: DraftModel): BlockSnapshot {
|
||||
this._slots.beforeExport.emit({
|
||||
type: 'block',
|
||||
model,
|
||||
});
|
||||
const schema = this._getSchema(model.flavour);
|
||||
const transformer = this._getTransformer(schema);
|
||||
const snapshotLeaf = transformer.toSnapshot({
|
||||
model,
|
||||
assets: this._assetsManager,
|
||||
});
|
||||
const children = model.children.map(child => {
|
||||
return this._blockToSnapshot(child);
|
||||
});
|
||||
const snapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
...snapshotLeaf,
|
||||
children,
|
||||
};
|
||||
this._slots.afterExport.emit({
|
||||
type: 'block',
|
||||
model,
|
||||
snapshot,
|
||||
});
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private async _convertFlatSnapshots(flatSnapshots: FlatSnapshot[]) {
|
||||
// Phase 1: Convert snapshots to draft models in series
|
||||
// This is not time-consuming, this is faster than Promise.all
|
||||
const draftModels = [];
|
||||
for (const flat of flatSnapshots) {
|
||||
const draft = await this._convertSnapshotToDraftModel(flat);
|
||||
if (draft) {
|
||||
draft.id = flat.snapshot.id;
|
||||
}
|
||||
draftModels.push({
|
||||
draft,
|
||||
snapshot: flat.snapshot,
|
||||
parentId: flat.parentId,
|
||||
index: flat.index,
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2: Filter out the models that failed to convert
|
||||
const validDraftModels = draftModels.filter(item => !!item.draft) as {
|
||||
draft: DraftModel;
|
||||
snapshot: BlockSnapshot;
|
||||
parentId?: string;
|
||||
index?: number;
|
||||
}[];
|
||||
|
||||
// Phase 3: Rebuild the block trees
|
||||
const blockTree = this._rebuildBlockTree(validDraftModels);
|
||||
return blockTree;
|
||||
}
|
||||
|
||||
private async _convertSnapshotToDraftModel(
|
||||
flat: FlatSnapshot
|
||||
): Promise<DraftModel | undefined> {
|
||||
try {
|
||||
const { children, flavour } = flat.snapshot;
|
||||
const schema = this._getSchema(flavour);
|
||||
const transformer = this._getTransformer(schema);
|
||||
const { props } = await transformer.fromSnapshot({
|
||||
json: {
|
||||
id: flat.snapshot.id,
|
||||
flavour: flat.snapshot.flavour,
|
||||
props: flat.snapshot.props,
|
||||
},
|
||||
assets: this._assetsManager,
|
||||
children,
|
||||
});
|
||||
|
||||
return {
|
||||
id: flat.snapshot.id,
|
||||
flavour: flat.snapshot.flavour,
|
||||
children: [],
|
||||
...props,
|
||||
} as DraftModel;
|
||||
} catch (error) {
|
||||
console.error(`Error when transforming snapshot to model data:`);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _exportDocMeta(doc: Doc): DocSnapshot['meta'] {
|
||||
const docMeta = doc.meta;
|
||||
|
||||
if (!docMeta) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
'Doc meta not found'
|
||||
);
|
||||
}
|
||||
return {
|
||||
id: docMeta.id,
|
||||
title: docMeta.title,
|
||||
createDate: docMeta.createDate,
|
||||
tags: [], // for backward compatibility
|
||||
};
|
||||
}
|
||||
|
||||
private _flattenSnapshot(
|
||||
snapshot: BlockSnapshot,
|
||||
flatSnapshots: FlatSnapshot[],
|
||||
parentId?: string,
|
||||
index?: number
|
||||
) {
|
||||
flatSnapshots.push({ snapshot, parentId, index });
|
||||
if (snapshot.children) {
|
||||
snapshot.children.forEach((child, idx) => {
|
||||
this._flattenSnapshot(child, flatSnapshots, snapshot.id, idx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _getCollectionMeta() {
|
||||
const { meta } = this._collection;
|
||||
const { docs } = meta;
|
||||
if (!docs) {
|
||||
throw new BlockSuiteError(ErrorCode.TransformerError, 'Docs not found');
|
||||
}
|
||||
return {
|
||||
properties: {}, // for backward compatibility
|
||||
pages: JSON.parse(JSON.stringify(docs)) as DocMeta[],
|
||||
};
|
||||
}
|
||||
|
||||
private _getSchema(flavour: string) {
|
||||
const schema = this._collection.schema.flavourSchemaMap.get(flavour);
|
||||
if (!schema) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
`Flavour schema not found for ${flavour}`
|
||||
);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
private _getTransformer(schema: BlockSchemaType) {
|
||||
return schema.transformer?.() ?? new BaseBlockTransformer();
|
||||
}
|
||||
|
||||
private async _insertBlockTree(
|
||||
nodes: DraftBlockTreeNode[],
|
||||
doc: Doc,
|
||||
parentId?: string,
|
||||
startIndex?: number,
|
||||
counter: number = 0
|
||||
) {
|
||||
for (let index = 0; index < nodes.length; index++) {
|
||||
const node = nodes[index];
|
||||
const { draft } = node;
|
||||
const { id, flavour } = draft;
|
||||
|
||||
const actualIndex =
|
||||
startIndex !== undefined ? startIndex + index : undefined;
|
||||
doc.addBlock(
|
||||
flavour as BlockSuite.Flavour,
|
||||
draft as object,
|
||||
parentId,
|
||||
actualIndex
|
||||
);
|
||||
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
`Block not found by id ${id}`
|
||||
);
|
||||
}
|
||||
|
||||
this._slots.afterImport.emit({
|
||||
type: 'block',
|
||||
model,
|
||||
snapshot: node.snapshot,
|
||||
});
|
||||
|
||||
counter++;
|
||||
if (counter % BATCH_SIZE === 0) {
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
if (node.children.length > 0) {
|
||||
counter = await this._insertBlockTree(
|
||||
node.children,
|
||||
doc,
|
||||
id,
|
||||
undefined,
|
||||
counter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
private _rebuildBlockTree(
|
||||
draftModels: {
|
||||
draft: DraftModel;
|
||||
snapshot: BlockSnapshot;
|
||||
parentId?: string;
|
||||
index?: number;
|
||||
}[]
|
||||
): DraftBlockTreeNode {
|
||||
const nodeMap = new Map<string, DraftBlockTreeNode>();
|
||||
// First pass: create nodes and add them to the map
|
||||
draftModels.forEach(({ draft, snapshot }) => {
|
||||
nodeMap.set(draft.id, { draft, snapshot, children: [] });
|
||||
});
|
||||
const root = nodeMap.get(draftModels[0].draft.id) as DraftBlockTreeNode;
|
||||
|
||||
// Second pass: build the tree structure
|
||||
draftModels.forEach(({ draft, parentId, index }) => {
|
||||
const node = nodeMap.get(draft.id);
|
||||
if (!node) return;
|
||||
|
||||
if (parentId) {
|
||||
const parentNode = nodeMap.get(parentId);
|
||||
if (parentNode && index !== undefined) {
|
||||
parentNode.children[index] = node;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!root) {
|
||||
throw new Error('No root node found in the tree');
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private async _snapshotToBlock(
|
||||
snapshot: BlockSnapshot,
|
||||
doc: Doc,
|
||||
parent?: string,
|
||||
index?: number
|
||||
): Promise<BlockModel | null> {
|
||||
this._triggerBeforeImportEvent(snapshot, parent, index);
|
||||
|
||||
const flatSnapshots: FlatSnapshot[] = [];
|
||||
this._flattenSnapshot(snapshot, flatSnapshots, parent, index);
|
||||
|
||||
const blockTree = await this._convertFlatSnapshots(flatSnapshots);
|
||||
|
||||
await this._insertBlockTree([blockTree], doc, parent, index);
|
||||
|
||||
return doc.getBlock(snapshot.id)?.model ?? null;
|
||||
}
|
||||
|
||||
private _triggerBeforeImportEvent(
|
||||
snapshot: BlockSnapshot,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) {
|
||||
const traverseAndTrigger = (
|
||||
node: BlockSnapshot,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) => {
|
||||
this._slots.beforeImport.emit({
|
||||
type: 'block',
|
||||
snapshot: node,
|
||||
parent: parent,
|
||||
index: index,
|
||||
});
|
||||
if (node.children) {
|
||||
node.children.forEach((child, idx) => {
|
||||
traverseAndTrigger(child, node.id, idx);
|
||||
});
|
||||
}
|
||||
};
|
||||
traverseAndTrigger(snapshot, parent, index);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
}
|
||||
51
blocksuite/framework/store/src/transformer/json.ts
Normal file
51
blocksuite/framework/store/src/transformer/json.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NATIVE_UNIQ_IDENTIFIER, TEXT_UNIQ_IDENTIFIER } from '../consts.js';
|
||||
import { Boxed } from '../reactive/boxed.js';
|
||||
import { isPureObject } from '../reactive/index.js';
|
||||
import { Text } from '../reactive/text.js';
|
||||
|
||||
export function toJSON(value: unknown): unknown {
|
||||
if (value instanceof Boxed) {
|
||||
return {
|
||||
[NATIVE_UNIQ_IDENTIFIER]: true,
|
||||
value: value.getValue(),
|
||||
};
|
||||
}
|
||||
if (value instanceof Text) {
|
||||
return {
|
||||
[TEXT_UNIQ_IDENTIFIER]: true,
|
||||
delta: value.yText.toDelta(),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(toJSON);
|
||||
}
|
||||
if (isPureObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => {
|
||||
return [key, toJSON(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function fromJSON(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(fromJSON);
|
||||
}
|
||||
if (typeof value === 'object' && value != null) {
|
||||
if (Reflect.has(value, NATIVE_UNIQ_IDENTIFIER)) {
|
||||
return new Boxed(Reflect.get(value, 'value'));
|
||||
}
|
||||
if (Reflect.has(value, TEXT_UNIQ_IDENTIFIER)) {
|
||||
return new Text(Reflect.get(value, 'delta'));
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => {
|
||||
return [key, fromJSON(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
88
blocksuite/framework/store/src/transformer/middleware.ts
Normal file
88
blocksuite/framework/store/src/transformer/middleware.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Slot } from '@blocksuite/global/utils';
|
||||
|
||||
import type { Doc, DocCollection } from '../store/index.js';
|
||||
import type { AssetsManager } from './assets.js';
|
||||
import type { DraftModel } from './draft.js';
|
||||
import type { Slice } from './slice.js';
|
||||
import type {
|
||||
BlockSnapshot,
|
||||
CollectionInfoSnapshot,
|
||||
DocSnapshot,
|
||||
SliceSnapshot,
|
||||
} from './type.js';
|
||||
|
||||
export type BeforeImportPayload =
|
||||
| {
|
||||
snapshot: BlockSnapshot;
|
||||
type: 'block';
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}
|
||||
| {
|
||||
snapshot: SliceSnapshot;
|
||||
type: 'slice';
|
||||
}
|
||||
| {
|
||||
snapshot: DocSnapshot;
|
||||
type: 'page';
|
||||
}
|
||||
| {
|
||||
snapshot: CollectionInfoSnapshot;
|
||||
type: 'info';
|
||||
};
|
||||
|
||||
export type BeforeExportPayload =
|
||||
| {
|
||||
model: DraftModel;
|
||||
type: 'block';
|
||||
}
|
||||
| {
|
||||
page: Doc;
|
||||
type: 'page';
|
||||
}
|
||||
| {
|
||||
slice: Slice;
|
||||
type: 'slice';
|
||||
}
|
||||
| {
|
||||
type: 'info';
|
||||
};
|
||||
|
||||
export type FinalPayload =
|
||||
| {
|
||||
snapshot: BlockSnapshot;
|
||||
type: 'block';
|
||||
model: DraftModel;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}
|
||||
| {
|
||||
snapshot: DocSnapshot;
|
||||
type: 'page';
|
||||
page: Doc;
|
||||
}
|
||||
| {
|
||||
snapshot: SliceSnapshot;
|
||||
type: 'slice';
|
||||
slice: Slice;
|
||||
}
|
||||
| {
|
||||
snapshot: CollectionInfoSnapshot;
|
||||
type: 'info';
|
||||
};
|
||||
|
||||
export type JobSlots = {
|
||||
beforeImport: Slot<BeforeImportPayload>;
|
||||
afterImport: Slot<FinalPayload>;
|
||||
beforeExport: Slot<BeforeExportPayload>;
|
||||
afterExport: Slot<FinalPayload>;
|
||||
};
|
||||
|
||||
type JobMiddlewareOptions = {
|
||||
collection: DocCollection;
|
||||
assetsManager: AssetsManager;
|
||||
slots: JobSlots;
|
||||
adapterConfigs: Map<string, string>;
|
||||
};
|
||||
|
||||
export type JobMiddleware = (options: JobMiddlewareOptions) => void;
|
||||
32
blocksuite/framework/store/src/transformer/slice.ts
Normal file
32
blocksuite/framework/store/src/transformer/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Doc } from '../store/index.js';
|
||||
import type { DraftModel } from './draft.js';
|
||||
|
||||
type SliceData = {
|
||||
content: DraftModel[];
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class Slice {
|
||||
get content() {
|
||||
return this.data.content;
|
||||
}
|
||||
|
||||
get docId() {
|
||||
return this.data.pageId;
|
||||
}
|
||||
|
||||
get workspaceId() {
|
||||
return this.data.workspaceId;
|
||||
}
|
||||
|
||||
constructor(readonly data: SliceData) {}
|
||||
|
||||
static fromModels(doc: Doc, models: DraftModel[]) {
|
||||
return new Slice({
|
||||
content: models,
|
||||
workspaceId: doc.collection.id,
|
||||
pageId: doc.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
67
blocksuite/framework/store/src/transformer/type.ts
Normal file
67
blocksuite/framework/store/src/transformer/type.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { DocMeta, DocsPropertiesMeta } from '../store/meta.js';
|
||||
|
||||
export type BlockSnapshot = {
|
||||
type: 'block';
|
||||
id: string;
|
||||
flavour: string;
|
||||
version?: number;
|
||||
props: Record<string, unknown>;
|
||||
children: BlockSnapshot[];
|
||||
};
|
||||
|
||||
export const BlockSnapshotSchema: z.ZodType<BlockSnapshot> = z.object({
|
||||
type: z.literal('block'),
|
||||
id: z.string(),
|
||||
flavour: z.string(),
|
||||
version: z.number().optional(),
|
||||
props: z.record(z.unknown()),
|
||||
children: z.lazy(() => BlockSnapshotSchema.array()),
|
||||
});
|
||||
|
||||
export type SliceSnapshot = {
|
||||
type: 'slice';
|
||||
content: BlockSnapshot[];
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export const SliceSnapshotSchema: z.ZodType<SliceSnapshot> = z.object({
|
||||
type: z.literal('slice'),
|
||||
content: BlockSnapshotSchema.array(),
|
||||
workspaceId: z.string(),
|
||||
pageId: z.string(),
|
||||
});
|
||||
|
||||
export type CollectionInfoSnapshot = {
|
||||
id: string;
|
||||
type: 'info';
|
||||
properties: DocsPropertiesMeta;
|
||||
};
|
||||
|
||||
export const CollectionInfoSnapshotSchema: z.ZodType<CollectionInfoSnapshot> =
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('info'),
|
||||
properties: z.record(z.any()),
|
||||
});
|
||||
|
||||
export type DocSnapshot = {
|
||||
type: 'page';
|
||||
meta: DocMeta;
|
||||
blocks: BlockSnapshot;
|
||||
};
|
||||
|
||||
const DocMetaSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
createDate: z.number(),
|
||||
tags: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const DocSnapshotSchema: z.ZodType<DocSnapshot> = z.object({
|
||||
type: z.literal('page'),
|
||||
meta: DocMetaSchema,
|
||||
blocks: BlockSnapshotSchema,
|
||||
});
|
||||
Reference in New Issue
Block a user