mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 06:16:59 +08:00
feat(infra): doc properties by orm (#8382)
create new orm table docConfiguration move primary store to docConfiguration
This commit is contained in:
@@ -4,7 +4,7 @@ import { WorkspaceDB } from './entities/db';
|
|||||||
import { WorkspaceDBTable } from './entities/table';
|
import { WorkspaceDBTable } from './entities/table';
|
||||||
import { WorkspaceDBService } from './services/db';
|
import { WorkspaceDBService } from './services/db';
|
||||||
|
|
||||||
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
|
export type { DocProperties } from './schema';
|
||||||
export { WorkspaceDBService } from './services/db';
|
export { WorkspaceDBService } from './services/db';
|
||||||
export { transformWorkspaceDBLocalToCloud } from './services/db';
|
export { transformWorkspaceDBLocalToCloud } from './services/db';
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
|
export type { DocProperties } from './schema';
|
||||||
|
export {
|
||||||
|
AFFiNE_WORKSPACE_DB_SCHEMA,
|
||||||
|
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
|
||||||
|
} from './schema';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { type DBSchemaBuilder, f } from '../../../orm';
|
import { type DBSchemaBuilder, f, type ORMEntity, t } from '../../../orm';
|
||||||
|
|
||||||
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||||
folders: {
|
folders: {
|
||||||
@@ -10,9 +10,34 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
|||||||
type: f.string(),
|
type: f.string(),
|
||||||
index: f.string(),
|
index: f.string(),
|
||||||
},
|
},
|
||||||
|
docProperties: t.document({
|
||||||
|
// { [`custom:{customPropertyId}`]: any }
|
||||||
|
id: f.string().primaryKey(),
|
||||||
|
primaryMode: f.string().optional(),
|
||||||
|
edgelessColorTheme: f.string().optional(),
|
||||||
|
journal: f.string().optional(),
|
||||||
|
}),
|
||||||
|
docCustomPropertyInfo: {
|
||||||
|
id: f.string().primaryKey().optional().default(nanoid),
|
||||||
|
name: f.string().optional(),
|
||||||
|
type: f.string(),
|
||||||
|
show: f.string().optional(),
|
||||||
|
index: f.string().optional(),
|
||||||
|
additionalData: f.json().optional(),
|
||||||
|
isDeleted: f.boolean().optional(),
|
||||||
|
// we will keep deleted properties in the database, for override legacy data
|
||||||
|
},
|
||||||
} as const satisfies DBSchemaBuilder;
|
} as const satisfies DBSchemaBuilder;
|
||||||
export type AFFiNE_WORKSPACE_DB_SCHEMA = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
export type AFFiNE_WORKSPACE_DB_SCHEMA = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||||
|
|
||||||
|
export type DocProperties = ORMEntity<
|
||||||
|
AFFiNE_WORKSPACE_DB_SCHEMA['docProperties']
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DocCustomPropertyInfo = ORMEntity<
|
||||||
|
AFFiNE_WORKSPACE_DB_SCHEMA['docCustomPropertyInfo']
|
||||||
|
>;
|
||||||
|
|
||||||
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
||||||
favorite: {
|
favorite: {
|
||||||
key: f.string().primaryKey(),
|
key: f.string().primaryKey(),
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import type { DocStorage } from '../../../sync';
|
|||||||
import { ObjectPool } from '../../../utils';
|
import { ObjectPool } from '../../../utils';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
|
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
|
||||||
import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema';
|
import {
|
||||||
import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema';
|
AFFiNE_WORKSPACE_DB_SCHEMA,
|
||||||
|
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
|
||||||
|
} from '../schema';
|
||||||
|
|
||||||
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
|
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
|
||||||
const WorkspaceUserdataDBClient = createORMClient(
|
const WorkspaceUserdataDBClient = createORMClient(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class Doc extends Entity {
|
|||||||
public readonly record = this.scope.props.record;
|
public readonly record = this.scope.props.record;
|
||||||
|
|
||||||
readonly meta$ = this.record.meta$;
|
readonly meta$ = this.record.meta$;
|
||||||
|
readonly properties$ = this.record.properties$;
|
||||||
readonly primaryMode$ = this.record.primaryMode$;
|
readonly primaryMode$ = this.record.primaryMode$;
|
||||||
readonly title$ = this.record.title$;
|
readonly title$ = this.record.title$;
|
||||||
readonly trash$ = this.record.trash$;
|
readonly trash$ = this.record.trash$;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Entity } from '../../../framework';
|
||||||
|
import { LiveData } from '../../../livedata';
|
||||||
|
import type { DocCustomPropertyInfo } from '../../db/schema/schema';
|
||||||
|
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||||
|
|
||||||
|
export class DocPropertyList extends Entity {
|
||||||
|
constructor(private readonly docPropertiesStore: DocPropertiesStore) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
properties$ = LiveData.from(
|
||||||
|
this.docPropertiesStore.watchDocPropertyInfoList(),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
updatePropertyInfo(id: string, properties: Partial<DocCustomPropertyInfo>) {
|
||||||
|
this.docPropertiesStore.updateDocPropertyInfo(id, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
createProperty(properties: DocCustomPropertyInfo) {
|
||||||
|
return this.docPropertiesStore.createDocPropertyInfo(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProperty(id: string) {
|
||||||
|
this.docPropertiesStore.removeDocPropertyInfo(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import type { DocMeta } from '@blocksuite/affine/store';
|
|||||||
|
|
||||||
import { Entity } from '../../../framework';
|
import { Entity } from '../../../framework';
|
||||||
import { LiveData } from '../../../livedata';
|
import { LiveData } from '../../../livedata';
|
||||||
|
import type { DocProperties } from '../../db';
|
||||||
|
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||||
import type { DocsStore } from '../stores/docs';
|
import type { DocsStore } from '../stores/docs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +14,10 @@ import type { DocsStore } from '../stores/docs';
|
|||||||
*/
|
*/
|
||||||
export class DocRecord extends Entity<{ id: string }> {
|
export class DocRecord extends Entity<{ id: string }> {
|
||||||
id: string = this.props.id;
|
id: string = this.props.id;
|
||||||
constructor(private readonly docsStore: DocsStore) {
|
constructor(
|
||||||
|
private readonly docsStore: DocsStore,
|
||||||
|
private readonly docPropertiesStore: DocPropertiesStore
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +26,15 @@ export class DocRecord extends Entity<{ id: string }> {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
properties$ = LiveData.from<DocProperties>(
|
||||||
|
this.docPropertiesStore.watchDocProperties(this.id),
|
||||||
|
{ id: this.id }
|
||||||
|
);
|
||||||
|
|
||||||
|
setProperties(properties: Partial<DocProperties>): void {
|
||||||
|
this.docPropertiesStore.updateDocProperties(this.id, properties);
|
||||||
|
}
|
||||||
|
|
||||||
setMeta(meta: Partial<DocMeta>): void {
|
setMeta(meta: Partial<DocMeta>): void {
|
||||||
this.docsStore.setDocMeta(this.id, meta);
|
this.docsStore.setDocMeta(this.id, meta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,27 @@ export { DocService } from './services/doc';
|
|||||||
export { DocsService } from './services/docs';
|
export { DocsService } from './services/docs';
|
||||||
|
|
||||||
import type { Framework } from '../../framework';
|
import type { Framework } from '../../framework';
|
||||||
import {
|
import { WorkspaceDBService } from '../db';
|
||||||
WorkspaceLocalState,
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
WorkspaceScope,
|
|
||||||
WorkspaceService,
|
|
||||||
} from '../workspace';
|
|
||||||
import { Doc } from './entities/doc';
|
import { Doc } from './entities/doc';
|
||||||
|
import { DocPropertyList } from './entities/property-list';
|
||||||
import { DocRecord } from './entities/record';
|
import { DocRecord } from './entities/record';
|
||||||
import { DocRecordList } from './entities/record-list';
|
import { DocRecordList } from './entities/record-list';
|
||||||
import { DocScope } from './scopes/doc';
|
import { DocScope } from './scopes/doc';
|
||||||
import { DocService } from './services/doc';
|
import { DocService } from './services/doc';
|
||||||
import { DocsService } from './services/docs';
|
import { DocsService } from './services/docs';
|
||||||
|
import { DocPropertiesStore } from './stores/doc-properties';
|
||||||
import { DocsStore } from './stores/docs';
|
import { DocsStore } from './stores/docs';
|
||||||
|
|
||||||
export function configureDocModule(framework: Framework) {
|
export function configureDocModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(DocsService, [DocsStore])
|
.service(DocsService, [DocsStore])
|
||||||
.store(DocsStore, [WorkspaceService, WorkspaceLocalState])
|
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
||||||
.entity(DocRecord, [DocsStore])
|
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
||||||
|
.entity(DocRecord, [DocsStore, DocPropertiesStore])
|
||||||
.entity(DocRecordList, [DocsStore])
|
.entity(DocRecordList, [DocsStore])
|
||||||
|
.entity(DocPropertyList, [DocPropertiesStore])
|
||||||
.scope(DocScope)
|
.scope(DocScope)
|
||||||
.entity(Doc, [DocScope, DocsStore, WorkspaceService])
|
.entity(Doc, [DocScope, DocsStore, WorkspaceService])
|
||||||
.service(DocService);
|
.service(DocService);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Service } from '../../../framework';
|
|||||||
import { type DocProps, initDocFromProps } from '../../../initialization';
|
import { type DocProps, initDocFromProps } from '../../../initialization';
|
||||||
import { ObjectPool } from '../../../utils';
|
import { ObjectPool } from '../../../utils';
|
||||||
import type { Doc } from '../entities/doc';
|
import type { Doc } from '../entities/doc';
|
||||||
|
import { DocPropertyList } from '../entities/property-list';
|
||||||
import { DocRecordList } from '../entities/record-list';
|
import { DocRecordList } from '../entities/record-list';
|
||||||
import { DocScope } from '../scopes/doc';
|
import { DocScope } from '../scopes/doc';
|
||||||
import type { DocsStore } from '../stores/docs';
|
import type { DocsStore } from '../stores/docs';
|
||||||
@@ -19,6 +20,8 @@ export class DocsService extends Service {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
propertyList = this.framework.createEntity(DocPropertyList);
|
||||||
|
|
||||||
constructor(private readonly store: DocsStore) {
|
constructor(private readonly store: DocsStore) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|||||||
232
packages/common/infra/src/modules/doc/stores/doc-properties.ts
Normal file
232
packages/common/infra/src/modules/doc/stores/doc-properties.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import { differenceBy, isNil, omitBy } from 'lodash-es';
|
||||||
|
import { combineLatest, map, switchMap } from 'rxjs';
|
||||||
|
import { AbstractType as YAbstractType } from 'yjs';
|
||||||
|
|
||||||
|
import { Store } from '../../../framework';
|
||||||
|
import {
|
||||||
|
yjsObserveByPath,
|
||||||
|
yjsObserveDeep,
|
||||||
|
} from '../../../utils/yjs-observable';
|
||||||
|
import type { WorkspaceDBService } from '../../db';
|
||||||
|
import type {
|
||||||
|
DocCustomPropertyInfo,
|
||||||
|
DocProperties,
|
||||||
|
} from '../../db/schema/schema';
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
|
||||||
|
interface LegacyDocProperties {
|
||||||
|
custom?: Record<string, { value: unknown } | undefined>;
|
||||||
|
system?: Record<string, { value: unknown } | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LegacyDocPropertyInfo = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LegacyDocPropertyInfoList = Record<
|
||||||
|
string,
|
||||||
|
LegacyDocPropertyInfo | undefined
|
||||||
|
>;
|
||||||
|
|
||||||
|
export class DocPropertiesStore extends Store {
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly dbService: WorkspaceDBService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDocProperties(id: string, config: Partial<DocProperties>) {
|
||||||
|
return this.dbService.db.docProperties.create({
|
||||||
|
id,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocPropertyInfoList() {
|
||||||
|
const db = this.dbService.db.docCustomPropertyInfo.find();
|
||||||
|
const legacy = this.upgradeLegacyDocPropertyInfoList(
|
||||||
|
this.getLegacyDocPropertyInfoList()
|
||||||
|
);
|
||||||
|
const notOverridden = differenceBy(legacy, db, i => i.id);
|
||||||
|
return [...db, ...notOverridden].filter(i => !i.isDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDocPropertyInfo(config: DocCustomPropertyInfo) {
|
||||||
|
return this.dbService.db.docCustomPropertyInfo.create(config).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDocPropertyInfo(id: string) {
|
||||||
|
this.updateDocPropertyInfo(id, {
|
||||||
|
additionalData: {}, // also remove additional data to reduce size
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDocPropertyInfo(id: string, config: Partial<DocCustomPropertyInfo>) {
|
||||||
|
const needMigration = !this.dbService.db.docCustomPropertyInfo.get(id);
|
||||||
|
if (needMigration) {
|
||||||
|
// if this property is not in db, we need to migration it from legacy to db, only type and name is needed
|
||||||
|
this.migrateLegacyDocPropertyInfo(id, config);
|
||||||
|
} else {
|
||||||
|
this.dbService.db.docCustomPropertyInfo.update(id, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateLegacyDocPropertyInfo(
|
||||||
|
id: string,
|
||||||
|
override: Partial<DocCustomPropertyInfo>
|
||||||
|
) {
|
||||||
|
const legacy = this.getLegacyDocPropertyInfo(id);
|
||||||
|
this.dbService.db.docCustomPropertyInfo.create({
|
||||||
|
id,
|
||||||
|
type:
|
||||||
|
legacy?.type ??
|
||||||
|
'unknown' /* should never reach here, just for safety, we need handle unknown property type */,
|
||||||
|
name: legacy?.name,
|
||||||
|
...override,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDocPropertyInfoList() {
|
||||||
|
return combineLatest([
|
||||||
|
this.watchLegacyDocPropertyInfoList().pipe(
|
||||||
|
map(this.upgradeLegacyDocPropertyInfoList)
|
||||||
|
),
|
||||||
|
this.dbService.db.docCustomPropertyInfo.find$({}),
|
||||||
|
]).pipe(
|
||||||
|
map(([legacy, db]) => {
|
||||||
|
const notOverridden = differenceBy(legacy, db, i => i.id);
|
||||||
|
return [...db, ...notOverridden].filter(i => !i.isDeleted);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocProperties(id: string) {
|
||||||
|
return {
|
||||||
|
...this.upgradeLegacyDocProperties(this.getLegacyDocProperties(id)),
|
||||||
|
...omitBy(this.dbService.db.docProperties.get(id), isNil),
|
||||||
|
// db always override legacy, but nil value should not override
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDocProperties(id: string) {
|
||||||
|
return combineLatest([
|
||||||
|
this.watchLegacyDocProperties(id).pipe(
|
||||||
|
map(this.upgradeLegacyDocProperties)
|
||||||
|
),
|
||||||
|
this.dbService.db.docProperties.get$(id),
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([legacy, db]) =>
|
||||||
|
({
|
||||||
|
...legacy,
|
||||||
|
...omitBy(db, isNil), // db always override legacy, but nil value should not override
|
||||||
|
}) as DocProperties
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private upgradeLegacyDocProperties(properties?: LegacyDocProperties) {
|
||||||
|
if (!properties) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const newProperties: Record<string, unknown> = {};
|
||||||
|
for (const [key, info] of Object.entries(properties.system ?? {})) {
|
||||||
|
if (info?.value !== undefined) {
|
||||||
|
newProperties[key] = info.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, info] of Object.entries(properties.custom ?? {})) {
|
||||||
|
if (info?.value !== undefined) {
|
||||||
|
newProperties['custom:' + key] = info.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private upgradeLegacyDocPropertyInfoList(
|
||||||
|
infoList?: LegacyDocPropertyInfoList
|
||||||
|
) {
|
||||||
|
if (!infoList) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInfoList: DocCustomPropertyInfo[] = [];
|
||||||
|
|
||||||
|
for (const [id, info] of Object.entries(infoList ?? {})) {
|
||||||
|
if (info?.type) {
|
||||||
|
newInfoList.push({
|
||||||
|
id,
|
||||||
|
name: info.name,
|
||||||
|
type: info.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newInfoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLegacyDocProperties(id: string) {
|
||||||
|
return this.workspaceService.workspace.rootYDoc
|
||||||
|
.getMap<any>('affine:workspace-properties')
|
||||||
|
.get('pageProperties')
|
||||||
|
?.get(id)
|
||||||
|
?.toJSON() as LegacyDocProperties | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private watchLegacyDocProperties(id: string) {
|
||||||
|
return yjsObserveByPath(
|
||||||
|
this.workspaceService.workspace.rootYDoc.getMap<any>(
|
||||||
|
'affine:workspace-properties'
|
||||||
|
),
|
||||||
|
`pageProperties.${id}`
|
||||||
|
).pipe(
|
||||||
|
switchMap(yjsObserveDeep),
|
||||||
|
map(
|
||||||
|
p =>
|
||||||
|
(p instanceof YAbstractType ? p.toJSON() : p) as
|
||||||
|
| LegacyDocProperties
|
||||||
|
| undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLegacyDocPropertyInfoList() {
|
||||||
|
return this.workspaceService.workspace.rootYDoc
|
||||||
|
.getMap<any>('affine:workspace-properties')
|
||||||
|
.get('schema')
|
||||||
|
?.get('pageProperties')
|
||||||
|
?.get('custom')
|
||||||
|
?.toJSON() as LegacyDocPropertyInfoList | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private watchLegacyDocPropertyInfoList() {
|
||||||
|
return yjsObserveByPath(
|
||||||
|
this.workspaceService.workspace.rootYDoc.getMap<any>(
|
||||||
|
'affine:workspace-properties'
|
||||||
|
),
|
||||||
|
'schema.pageProperties.custom'
|
||||||
|
).pipe(
|
||||||
|
switchMap(yjsObserveDeep),
|
||||||
|
map(
|
||||||
|
p =>
|
||||||
|
(p instanceof YAbstractType ? p.toJSON() : p) as
|
||||||
|
| LegacyDocPropertyInfoList
|
||||||
|
| undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLegacyDocPropertyInfo(id: string) {
|
||||||
|
return this.workspaceService.workspace.rootYDoc
|
||||||
|
.getMap<any>('affine:workspace-properties')
|
||||||
|
.get('schema')
|
||||||
|
?.get('pageProperties')
|
||||||
|
?.get('custom')
|
||||||
|
?.get(id)
|
||||||
|
?.toJSON() as LegacyDocPropertyInfo | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { isEqual } from 'lodash-es';
|
import { distinctUntilChanged, map, switchMap } from 'rxjs';
|
||||||
import { distinctUntilChanged, Observable } from 'rxjs';
|
import { Array as YArray, Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { Store } from '../../../framework';
|
import { Store } from '../../../framework';
|
||||||
import type { WorkspaceLocalState, WorkspaceService } from '../../workspace';
|
import { yjsObserve, yjsObserveByPath, yjsObserveDeep } from '../../../utils';
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
import type { DocPropertiesStore } from './doc-properties';
|
||||||
|
|
||||||
export class DocsStore extends Store {
|
export class DocsStore extends Store {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly localState: WorkspaceLocalState
|
private readonly docPropertiesStore: DocPropertiesStore
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -23,72 +25,67 @@ export class DocsStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchDocIds() {
|
watchDocIds() {
|
||||||
return new Observable<string[]>(subscriber => {
|
return yjsObserveByPath(
|
||||||
const emit = () => {
|
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||||
subscriber.next(
|
'pages'
|
||||||
this.workspaceService.workspace.docCollection.meta.docMetas.map(
|
).pipe(
|
||||||
v => v.id
|
switchMap(yjsObserve),
|
||||||
)
|
map(meta => {
|
||||||
);
|
if (meta instanceof YArray) {
|
||||||
};
|
return meta.map(v => v.get('id'));
|
||||||
|
} else {
|
||||||
emit();
|
return [];
|
||||||
|
}
|
||||||
const dispose =
|
})
|
||||||
this.workspaceService.workspace.docCollection.meta.docMetaUpdated.on(
|
);
|
||||||
emit
|
|
||||||
).dispose;
|
|
||||||
return () => {
|
|
||||||
dispose();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTrashDocIds() {
|
watchTrashDocIds() {
|
||||||
return new Observable<string[]>(subscriber => {
|
return yjsObserveByPath(
|
||||||
const emit = () => {
|
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||||
subscriber.next(
|
'pages'
|
||||||
this.workspaceService.workspace.docCollection.meta.docMetas
|
).pipe(
|
||||||
.map(v => (v.trash ? v.id : null))
|
switchMap(yjsObserveDeep),
|
||||||
.filter(Boolean) as string[]
|
map(meta => {
|
||||||
);
|
if (meta instanceof YArray) {
|
||||||
};
|
return meta
|
||||||
|
.map(v => (v.get('trash') ? v.get('id') : null))
|
||||||
emit();
|
.filter(Boolean) as string[];
|
||||||
|
} else {
|
||||||
const dispose =
|
return [];
|
||||||
this.workspaceService.workspace.docCollection.meta.docMetaUpdated.on(
|
}
|
||||||
emit
|
})
|
||||||
).dispose;
|
);
|
||||||
return () => {
|
|
||||||
dispose();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDocMeta(id: string) {
|
watchDocMeta(id: string) {
|
||||||
let meta: DocMeta | null = null;
|
return yjsObserveByPath(
|
||||||
return new Observable<Partial<DocMeta>>(subscriber => {
|
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||||
const emit = () => {
|
'pages'
|
||||||
if (meta === null) {
|
).pipe(
|
||||||
// getDocMeta is heavy, so we cache the doc meta reference
|
switchMap(yjsObserve),
|
||||||
meta =
|
map(meta => {
|
||||||
this.workspaceService.workspace.docCollection.meta.getDocMeta(id) ||
|
if (meta instanceof YArray) {
|
||||||
null;
|
let docMetaYMap = null as YMap<any> | null;
|
||||||
|
meta.forEach(doc => {
|
||||||
|
if (doc.get('id') === id) {
|
||||||
|
docMetaYMap = doc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return docMetaYMap;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
subscriber.next({ ...meta });
|
}),
|
||||||
};
|
switchMap(yjsObserveDeep),
|
||||||
|
map(meta => {
|
||||||
emit();
|
if (meta instanceof YMap) {
|
||||||
|
return meta.toJSON() as Partial<DocMeta>;
|
||||||
const dispose =
|
} else {
|
||||||
this.workspaceService.workspace.docCollection.meta.docMetaUpdated.on(
|
return {};
|
||||||
emit
|
}
|
||||||
).dispose;
|
})
|
||||||
return () => {
|
);
|
||||||
dispose();
|
|
||||||
};
|
|
||||||
}).pipe(distinctUntilChanged((p, c) => isEqual(p, c)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDocListReady() {
|
watchDocListReady() {
|
||||||
@@ -102,15 +99,20 @@ export class DocsStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDocPrimaryModeSetting(id: string, mode: DocMode) {
|
setDocPrimaryModeSetting(id: string, mode: DocMode) {
|
||||||
return this.localState.set(`page:${id}:mode`, mode);
|
return this.docPropertiesStore.updateDocProperties(id, {
|
||||||
|
primaryMode: mode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocPrimaryModeSetting(id: string) {
|
getDocPrimaryModeSetting(id: string) {
|
||||||
return this.localState.get<DocMode>(`page:${id}:mode`);
|
return this.docPropertiesStore.getDocProperties(id)?.primaryMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDocPrimaryModeSetting(id: string) {
|
watchDocPrimaryModeSetting(id: string) {
|
||||||
return this.localState.watch<DocMode>(`page:${id}:mode`);
|
return this.docPropertiesStore.watchDocProperties(id).pipe(
|
||||||
|
map(config => config?.primaryMode),
|
||||||
|
distinctUntilChanged((p, c) => p === c)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForDocLoadReady(id: string) {
|
waitForDocLoadReady(id: string) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Awareness } from 'y-protocols/awareness.js';
|
|||||||
|
|
||||||
import { Entity } from '../../../framework';
|
import { Entity } from '../../../framework';
|
||||||
import { LiveData } from '../../../livedata';
|
import { LiveData } from '../../../livedata';
|
||||||
|
import { WorkspaceDBService } from '../../db';
|
||||||
import { getAFFiNEWorkspaceSchema } from '../global-schema';
|
import { getAFFiNEWorkspaceSchema } from '../global-schema';
|
||||||
import type { WorkspaceScope } from '../scopes/workspace';
|
import type { WorkspaceScope } from '../scopes/workspace';
|
||||||
import { WorkspaceEngineService } from '../services/engine';
|
import { WorkspaceEngineService } from '../services/engine';
|
||||||
@@ -42,6 +43,10 @@ export class Workspace extends Entity {
|
|||||||
return this._docCollection;
|
return this._docCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.framework.get(WorkspaceDBService).db;
|
||||||
|
}
|
||||||
|
|
||||||
get awareness() {
|
get awareness() {
|
||||||
return this.docCollection.awarenessStore.awareness as Awareness;
|
return this.docCollection.awarenessStore.awareness as Awareness;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ export type {
|
|||||||
DBSchemaBuilder,
|
DBSchemaBuilder,
|
||||||
FieldSchemaBuilder,
|
FieldSchemaBuilder,
|
||||||
ORMClient,
|
ORMClient,
|
||||||
|
Entity as ORMEntity,
|
||||||
Table,
|
Table,
|
||||||
TableMap,
|
TableMap,
|
||||||
TableSchemaBuilder,
|
TableSchemaBuilder,
|
||||||
|
UpdateEntityInput,
|
||||||
} from './core';
|
} from './core';
|
||||||
export { createORMClient, f, YjsDBAdapter } from './core';
|
export { createORMClient, f, t, YjsDBAdapter } from './core';
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
|
|
||||||
|
import { yjsObserveByPath } from '../yjs-observable';
|
||||||
|
|
||||||
|
describe('yjs observable', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
const ydoc = new YDoc();
|
||||||
|
let currentValue: any = false;
|
||||||
|
yjsObserveByPath(ydoc.getMap('foo'), 'key.subkey').subscribe(
|
||||||
|
v => (currentValue = v)
|
||||||
|
);
|
||||||
|
expect(currentValue).toBe(undefined);
|
||||||
|
|
||||||
|
ydoc.getMap('foo').set('key', new YMap([['subkey', 'xxxzzz']]));
|
||||||
|
expect(currentValue).toBe('xxxzzz');
|
||||||
|
|
||||||
|
(ydoc.getMap('foo').get('key') as YMap<string>).set('subkey', 'yyy');
|
||||||
|
expect(currentValue).toBe('yyy');
|
||||||
|
|
||||||
|
(ydoc.getMap('foo').get('key') as YMap<string>).delete('subkey');
|
||||||
|
expect(currentValue).toBe(undefined);
|
||||||
|
|
||||||
|
(ydoc.getMap('foo').get('key') as YMap<string>).set('subkey', 'yyy');
|
||||||
|
ydoc.getMap('foo').delete('key');
|
||||||
|
expect(currentValue).toBe(undefined);
|
||||||
|
|
||||||
|
ydoc.getMap('foo').set('key', 'text');
|
||||||
|
expect(currentValue).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,3 +5,4 @@ export * from './merge-updates';
|
|||||||
export * from './object-pool';
|
export * from './object-pool';
|
||||||
export * from './stable-hash';
|
export * from './stable-hash';
|
||||||
export * from './throw-if-aborted';
|
export * from './throw-if-aborted';
|
||||||
|
export * from './yjs-observable';
|
||||||
|
|||||||
121
packages/common/infra/src/utils/yjs-observable.ts
Normal file
121
packages/common/infra/src/utils/yjs-observable.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { distinctUntilChanged, Observable, of, switchMap } from 'rxjs';
|
||||||
|
import {
|
||||||
|
AbstractType as YAbstractType,
|
||||||
|
Array as YArray,
|
||||||
|
Map as YMap,
|
||||||
|
} from 'yjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param path key.[0].key2.[1]
|
||||||
|
*/
|
||||||
|
function parsePath(path: string): (string | number)[] {
|
||||||
|
const parts = path.split('.');
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part.startsWith('[') && part.endsWith(']')) {
|
||||||
|
const index = parseInt(part.slice(1, -1), 10);
|
||||||
|
if (isNaN(index)) {
|
||||||
|
throw new Error(`index: ${part} is not a number`);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _yjsDeepWatch(
|
||||||
|
target: any,
|
||||||
|
path: ReturnType<typeof parsePath>
|
||||||
|
): Observable<unknown | undefined> {
|
||||||
|
if (path.length === 0) {
|
||||||
|
return of(target);
|
||||||
|
}
|
||||||
|
const current = path[0];
|
||||||
|
|
||||||
|
if (target instanceof YArray || target instanceof YMap) {
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
const refresh = () => {
|
||||||
|
if (typeof current === 'number' && target instanceof YArray) {
|
||||||
|
subscriber.next(target.get(current));
|
||||||
|
} else if (typeof current === 'string' && target instanceof YMap) {
|
||||||
|
subscriber.next(target.get(current));
|
||||||
|
} else {
|
||||||
|
subscriber.next(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
refresh();
|
||||||
|
target.observe(refresh);
|
||||||
|
return () => {
|
||||||
|
target.unobserve(refresh);
|
||||||
|
};
|
||||||
|
}).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap(arr => _yjsDeepWatch(arr, path.slice(1)))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract data from yjs type based on path, and return an observable.
|
||||||
|
* observable will automatically update when yjs data changed.
|
||||||
|
* if data is not exist on path, the observable will emit undefined.
|
||||||
|
*
|
||||||
|
* this function is optimized for deep watch performance.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* yjsObserveByPath(yjs, 'pages.[0].id') -> only emit when pages[0].id changed
|
||||||
|
* yjsObserveByPath(yjs, 'pages.[0]').switchMap(yjsObserve) -> emit when any of pages[0] or its children changed
|
||||||
|
* yjsObserveByPath(yjs, 'pages.[0]').switchMap(yjsObserveDeep) -> emit when pages[0] or any of its deep children changed
|
||||||
|
*/
|
||||||
|
export function yjsObserveByPath(yjs: YAbstractType<any>, path: string) {
|
||||||
|
const parsedPath = parsePath(path);
|
||||||
|
return _yjsDeepWatch(yjs, parsedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert yjs type to observable.
|
||||||
|
* observable will automatically update when yjs data changed.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* yjsObserveDeep(yjs) -> emit when any of its deep children changed
|
||||||
|
*/
|
||||||
|
export function yjsObserveDeep(yjs?: any) {
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
const refresh = () => {
|
||||||
|
subscriber.next(yjs);
|
||||||
|
};
|
||||||
|
refresh();
|
||||||
|
if (yjs instanceof YAbstractType) {
|
||||||
|
yjs.observeDeep(refresh);
|
||||||
|
return () => {
|
||||||
|
yjs.unobserveDeep(refresh);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert yjs type to observable.
|
||||||
|
* observable will automatically update when yjs data changed.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* yjsObserveDeep(yjs) -> emit when any of children changed
|
||||||
|
*/
|
||||||
|
export function yjsObserve(yjs?: any) {
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
const refresh = () => {
|
||||||
|
subscriber.next(yjs);
|
||||||
|
};
|
||||||
|
refresh();
|
||||||
|
if (yjs instanceof YAbstractType) {
|
||||||
|
yjs.observe(refresh);
|
||||||
|
return () => {
|
||||||
|
yjs.unobserve(refresh);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user