mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(core): move infra modules to core (#9207)
This commit is contained in:
@@ -4,44 +4,7 @@ export * from './blocksuite';
|
||||
export * from './framework';
|
||||
export * from './initialization';
|
||||
export * from './livedata';
|
||||
export * from './modules/db';
|
||||
export * from './modules/doc';
|
||||
export * from './modules/feature-flag';
|
||||
export * from './modules/global-context';
|
||||
export * from './modules/lifecycle';
|
||||
export * from './modules/storage';
|
||||
export * from './modules/workspace';
|
||||
export * from './orm';
|
||||
export * from './storage';
|
||||
export * from './sync';
|
||||
export * from './utils';
|
||||
|
||||
import type { Framework } from './framework';
|
||||
import { configureWorkspaceDBModule } from './modules/db';
|
||||
import { configureDocModule } from './modules/doc';
|
||||
import { configureFeatureFlagModule } from './modules/feature-flag';
|
||||
import { configureGlobalContextModule } from './modules/global-context';
|
||||
import { configureLifecycleModule } from './modules/lifecycle';
|
||||
import {
|
||||
configureGlobalStorageModule,
|
||||
configureTestingGlobalStorage,
|
||||
} from './modules/storage';
|
||||
import {
|
||||
configureTestingWorkspaceProvider,
|
||||
configureWorkspaceModule,
|
||||
} from './modules/workspace';
|
||||
|
||||
export function configureInfraModules(framework: Framework) {
|
||||
configureWorkspaceModule(framework);
|
||||
configureDocModule(framework);
|
||||
configureWorkspaceDBModule(framework);
|
||||
configureGlobalStorageModule(framework);
|
||||
configureGlobalContextModule(framework);
|
||||
configureLifecycleModule(framework);
|
||||
configureFeatureFlagModule(framework);
|
||||
}
|
||||
|
||||
export function configureTestingInfraModules(framework: Framework) {
|
||||
configureTestingGlobalStorage(framework);
|
||||
configureTestingWorkspaceProvider(framework);
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Entity } from '../../../framework';
|
||||
import type { DBSchemaBuilder, TableMap } from '../../../orm';
|
||||
import { WorkspaceDBTable } from './table';
|
||||
|
||||
export class WorkspaceDB<Schema extends DBSchemaBuilder> extends Entity<{
|
||||
db: TableMap<Schema>;
|
||||
schema: Schema;
|
||||
storageDocId: (tableName: string) => string;
|
||||
}> {
|
||||
readonly db = this.props.db;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
Object.entries(this.props.schema).forEach(([tableName]) => {
|
||||
const table = this.framework.createEntity(WorkspaceDBTable, {
|
||||
table: this.db[tableName],
|
||||
storageDocId: this.props.storageDocId(tableName),
|
||||
});
|
||||
Object.defineProperty(this, tableName, {
|
||||
get: () => table,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkspaceDBWithTables<Schema extends DBSchemaBuilder> =
|
||||
WorkspaceDB<Schema> & {
|
||||
[K in keyof Schema]: WorkspaceDBTable<Schema[K]>;
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Entity } from '../../../framework';
|
||||
import type { Table as OrmTable, TableSchemaBuilder } from '../../../orm';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
|
||||
export class WorkspaceDBTable<
|
||||
Schema extends TableSchemaBuilder,
|
||||
> extends Entity<{
|
||||
table: OrmTable<Schema>;
|
||||
storageDocId: string;
|
||||
}> {
|
||||
readonly table = this.props.table;
|
||||
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
}
|
||||
|
||||
isSyncing$ = this.workspaceService.workspace.engine.doc
|
||||
.docState$(this.props.storageDocId)
|
||||
.map(docState => docState.syncing);
|
||||
|
||||
isLoading$ = this.workspaceService.workspace.engine.doc
|
||||
.docState$(this.props.storageDocId)
|
||||
.map(docState => docState.loading);
|
||||
|
||||
create = this.table.create.bind(this.table);
|
||||
update = this.table.update.bind(this.table);
|
||||
get = this.table.get.bind(this.table);
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
get$ = this.table.get$.bind(this.table);
|
||||
find = this.table.find.bind(this.table);
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
find$ = this.table.find$.bind(this.table);
|
||||
keys = this.table.keys.bind(this.table);
|
||||
delete = this.table.delete.bind(this.table);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Framework } from '../../framework';
|
||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||
import { WorkspaceDB } from './entities/db';
|
||||
import { WorkspaceDBTable } from './entities/table';
|
||||
import { WorkspaceDBService } from './services/db';
|
||||
|
||||
export type { DocCustomPropertyInfo, DocProperties } from './schema';
|
||||
export { WorkspaceDBService } from './services/db';
|
||||
export { transformWorkspaceDBLocalToCloud } from './services/db';
|
||||
|
||||
export function configureWorkspaceDBModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceDBService, [WorkspaceService])
|
||||
.entity(WorkspaceDB)
|
||||
.entity(WorkspaceDBTable, [WorkspaceService]);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export type { DocCustomPropertyInfo, DocProperties } from './schema';
|
||||
export {
|
||||
AFFiNE_WORKSPACE_DB_SCHEMA,
|
||||
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
|
||||
type AFFiNEWorkspaceDbSchema,
|
||||
type AFFiNEWorkspaceUserdataDbSchema,
|
||||
} from './schema';
|
||||
@@ -1,48 +0,0 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { type DBSchemaBuilder, f, type ORMEntity, t } from '../../../orm';
|
||||
|
||||
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
|
||||
folders: {
|
||||
id: f.string().primaryKey().optional().default(nanoid),
|
||||
parentId: f.string().optional(),
|
||||
data: f.string(),
|
||||
type: 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(),
|
||||
pageWidth: 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(),
|
||||
icon: 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;
|
||||
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
|
||||
|
||||
export type DocProperties = ORMEntity<AFFiNEWorkspaceDbSchema['docProperties']>;
|
||||
|
||||
export type DocCustomPropertyInfo = ORMEntity<
|
||||
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
|
||||
>;
|
||||
|
||||
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
|
||||
favorite: {
|
||||
key: f.string().primaryKey(),
|
||||
index: f.string(),
|
||||
},
|
||||
} as const satisfies DBSchemaBuilder;
|
||||
export type AFFiNEWorkspaceUserdataDbSchema =
|
||||
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { createORMClient, YjsDBAdapter } from '../../../orm';
|
||||
import type { DocStorage } from '../../../sync';
|
||||
import { ObjectPool } from '../../../utils';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
|
||||
import {
|
||||
AFFiNE_WORKSPACE_DB_SCHEMA,
|
||||
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
|
||||
type AFFiNEWorkspaceDbSchema,
|
||||
type AFFiNEWorkspaceUserdataDbSchema,
|
||||
} from '../schema';
|
||||
|
||||
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
|
||||
const WorkspaceUserdataDBClient = createORMClient(
|
||||
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA
|
||||
);
|
||||
|
||||
export class WorkspaceDBService extends Service {
|
||||
db: WorkspaceDBWithTables<AFFiNEWorkspaceDbSchema>;
|
||||
userdataDBPool = new ObjectPool<
|
||||
string,
|
||||
WorkspaceDB<AFFiNEWorkspaceUserdataDbSchema>
|
||||
>({
|
||||
onDangling() {
|
||||
return false; // never release
|
||||
},
|
||||
});
|
||||
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
this.db = this.framework.createEntity(
|
||||
WorkspaceDB<AFFiNEWorkspaceDbSchema>,
|
||||
{
|
||||
db: new WorkspaceDBClient(
|
||||
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
|
||||
getDoc: guid => {
|
||||
const ydoc = new YDoc({
|
||||
// guid format: db${workspaceId}${guid}
|
||||
guid: `db$${this.workspaceService.workspace.id}$${guid}`,
|
||||
});
|
||||
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
|
||||
this.workspaceService.workspace.engine.doc.setPriority(
|
||||
ydoc.guid,
|
||||
50
|
||||
);
|
||||
return ydoc;
|
||||
},
|
||||
})
|
||||
),
|
||||
schema: AFFiNE_WORKSPACE_DB_SCHEMA,
|
||||
storageDocId: tableName =>
|
||||
`db$${this.workspaceService.workspace.id}$${tableName}`,
|
||||
}
|
||||
) as WorkspaceDBWithTables<AFFiNEWorkspaceDbSchema>;
|
||||
}
|
||||
|
||||
userdataDB(userId: (string & {}) | '__local__') {
|
||||
// __local__ for local workspace
|
||||
const userdataDb = this.userdataDBPool.get(userId);
|
||||
if (userdataDb) {
|
||||
return userdataDb.obj as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
|
||||
}
|
||||
|
||||
const newDB = this.framework.createEntity(
|
||||
WorkspaceDB<AFFiNEWorkspaceUserdataDbSchema>,
|
||||
{
|
||||
db: new WorkspaceUserdataDBClient(
|
||||
new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, {
|
||||
getDoc: guid => {
|
||||
const ydoc = new YDoc({
|
||||
// guid format: userdata${userId}${workspaceId}${guid}
|
||||
guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`,
|
||||
});
|
||||
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
|
||||
this.workspaceService.workspace.engine.doc.setPriority(
|
||||
ydoc.guid,
|
||||
50
|
||||
);
|
||||
return ydoc;
|
||||
},
|
||||
})
|
||||
),
|
||||
schema: AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
|
||||
storageDocId: tableName =>
|
||||
`userdata$${userId}$${this.workspaceService.workspace.id}$${tableName}`,
|
||||
}
|
||||
);
|
||||
|
||||
this.userdataDBPool.put(userId, newDB);
|
||||
return newDB as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
|
||||
}
|
||||
|
||||
static isDBDocId(docId: string) {
|
||||
return docId.startsWith('db$') || docId.startsWith('userdata$');
|
||||
}
|
||||
}
|
||||
|
||||
export async function transformWorkspaceDBLocalToCloud(
|
||||
localWorkspaceId: string,
|
||||
cloudWorkspaceId: string,
|
||||
localDocStorage: DocStorage,
|
||||
cloudDocStorage: DocStorage,
|
||||
accountId: string
|
||||
) {
|
||||
for (const tableName of Object.keys(AFFiNE_WORKSPACE_DB_SCHEMA)) {
|
||||
const localDocName = `db$${localWorkspaceId}$${tableName}`;
|
||||
const localDoc = await localDocStorage.doc.get(localDocName);
|
||||
if (localDoc) {
|
||||
const cloudDocName = `db$${cloudWorkspaceId}$${tableName}`;
|
||||
await cloudDocStorage.doc.set(cloudDocName, localDoc);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tableName of Object.keys(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA)) {
|
||||
const localDocName = `userdata$__local__$${localWorkspaceId}$${tableName}`;
|
||||
const localDoc = await localDocStorage.doc.get(localDocName);
|
||||
if (localDoc) {
|
||||
const cloudDocName = `userdata$${accountId}$${cloudWorkspaceId}$${tableName}`;
|
||||
await cloudDocStorage.doc.set(cloudDocName, localDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { DocCustomPropertyInfo } from '../db';
|
||||
|
||||
/**
|
||||
* default built-in custom property, user can update and delete them
|
||||
*
|
||||
* 'id' and 'type' is request, 'index' is a manually maintained incremental key.
|
||||
*/
|
||||
export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
|
||||
{
|
||||
id: 'tags',
|
||||
type: 'tags',
|
||||
index: 'a0000001',
|
||||
},
|
||||
{
|
||||
id: 'docPrimaryMode',
|
||||
type: 'docPrimaryMode',
|
||||
show: 'always-hide',
|
||||
index: 'a0000002',
|
||||
},
|
||||
{
|
||||
id: 'journal',
|
||||
type: 'journal',
|
||||
show: 'always-hide',
|
||||
index: 'a0000003',
|
||||
},
|
||||
{
|
||||
id: 'createdAt',
|
||||
type: 'createdAt',
|
||||
index: 'a0000004',
|
||||
},
|
||||
{
|
||||
id: 'updatedAt',
|
||||
type: 'updatedAt',
|
||||
index: 'a0000005',
|
||||
},
|
||||
{
|
||||
id: 'createdBy',
|
||||
type: 'createdBy',
|
||||
show: 'always-hide',
|
||||
index: 'a0000006',
|
||||
},
|
||||
{
|
||||
id: 'edgelessTheme',
|
||||
type: 'edgelessTheme',
|
||||
show: 'always-hide',
|
||||
index: 'a0000007',
|
||||
},
|
||||
{
|
||||
id: 'pageWidth',
|
||||
type: 'pageWidth',
|
||||
show: 'always-hide',
|
||||
index: 'a0000008',
|
||||
},
|
||||
] as DocCustomPropertyInfo[];
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { DocMode, RootBlockModel } from '@blocksuite/affine/blocks';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
|
||||
export class Doc extends Entity {
|
||||
constructor(
|
||||
public readonly scope: DocScope,
|
||||
private readonly store: DocsStore,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* for convenience
|
||||
*/
|
||||
get workspace() {
|
||||
return this.workspaceService.workspace;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.scope.props.docId;
|
||||
}
|
||||
|
||||
public readonly blockSuiteDoc = this.scope.props.blockSuiteDoc;
|
||||
public readonly record = this.scope.props.record;
|
||||
|
||||
readonly meta$ = this.record.meta$;
|
||||
readonly properties$ = this.record.properties$;
|
||||
readonly primaryMode$ = this.record.primaryMode$;
|
||||
readonly title$ = this.record.title$;
|
||||
readonly trash$ = this.record.trash$;
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.record.customProperty$(propertyId);
|
||||
}
|
||||
|
||||
setCustomProperty(propertyId: string, value: string) {
|
||||
return this.record.setCustomProperty(propertyId, value);
|
||||
}
|
||||
|
||||
setPrimaryMode(mode: DocMode) {
|
||||
return this.record.setPrimaryMode(mode);
|
||||
}
|
||||
|
||||
getPrimaryMode() {
|
||||
return this.record.getPrimaryMode();
|
||||
}
|
||||
|
||||
togglePrimaryMode() {
|
||||
this.setPrimaryMode(
|
||||
(this.getPrimaryMode() === 'edgeless' ? 'page' : 'edgeless') as DocMode
|
||||
);
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
return this.record.moveToTrash();
|
||||
}
|
||||
|
||||
restoreFromTrash() {
|
||||
return this.record.restoreFromTrash();
|
||||
}
|
||||
|
||||
waitForSyncReady() {
|
||||
return this.store.waitForDocLoadReady(this.id);
|
||||
}
|
||||
|
||||
setPriorityLoad(priority: number) {
|
||||
return this.store.setPriorityLoad(this.id, priority);
|
||||
}
|
||||
|
||||
changeDocTitle(newTitle: string) {
|
||||
const pageBlock = this.blockSuiteDoc.getBlocksByFlavour('affine:page').at(0)
|
||||
?.model as RootBlockModel | undefined;
|
||||
if (pageBlock) {
|
||||
this.blockSuiteDoc.transact(() => {
|
||||
pageBlock.title.delete(0, pageBlock.title.length);
|
||||
pageBlock.title.insert(newTitle, 0);
|
||||
});
|
||||
this.record.setMeta({ title: newTitle });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import { generateFractionalIndexingKeyBetween } from '../../../utils';
|
||||
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(),
|
||||
[]
|
||||
);
|
||||
|
||||
sortedProperties$ = this.properties$.map(list =>
|
||||
// default index key is '', so always before any others
|
||||
list.toSorted((a, b) => ((a.index ?? '') > (b.index ?? '') ? 1 : -1))
|
||||
);
|
||||
|
||||
propertyInfo$(id: string) {
|
||||
return this.properties$.map(list => list.find(info => info.id === id));
|
||||
}
|
||||
|
||||
updatePropertyInfo(id: string, properties: Partial<DocCustomPropertyInfo>) {
|
||||
this.docPropertiesStore.updateDocPropertyInfo(id, properties);
|
||||
}
|
||||
|
||||
createProperty(
|
||||
properties: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
|
||||
) {
|
||||
return this.docPropertiesStore.createDocPropertyInfo(properties);
|
||||
}
|
||||
|
||||
removeProperty(id: string) {
|
||||
this.docPropertiesStore.removeDocPropertyInfo(id);
|
||||
}
|
||||
|
||||
indexAt(at: 'before' | 'after', targetId?: string) {
|
||||
const sortedChildren = this.sortedProperties$.value.filter(
|
||||
node => node.index
|
||||
) as (DocCustomPropertyInfo & { index: string })[];
|
||||
const targetIndex = targetId
|
||||
? sortedChildren.findIndex(node => node.id === targetId)
|
||||
: -1;
|
||||
if (targetIndex === -1) {
|
||||
if (at === 'before') {
|
||||
const first = sortedChildren.at(0);
|
||||
return generateFractionalIndexingKeyBetween(null, first?.index ?? null);
|
||||
} else {
|
||||
const last = sortedChildren.at(-1);
|
||||
return generateFractionalIndexingKeyBetween(last?.index ?? null, null);
|
||||
}
|
||||
} else {
|
||||
const target = sortedChildren[targetIndex];
|
||||
const before: DocCustomPropertyInfo | null =
|
||||
sortedChildren[targetIndex - 1] || null;
|
||||
const after: DocCustomPropertyInfo | null =
|
||||
sortedChildren[targetIndex + 1] || null;
|
||||
if (at === 'before') {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
before?.index ?? null,
|
||||
target.index
|
||||
);
|
||||
} else {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
target.index,
|
||||
after?.index ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocRecord } from './record';
|
||||
|
||||
export class DocRecordList extends Entity {
|
||||
constructor(private readonly store: DocsStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
private readonly pool = new Map<string, DocRecord>();
|
||||
|
||||
public readonly docsMap$ = LiveData.from<Map<string, DocRecord>>(
|
||||
this.store.watchDocIds().pipe(
|
||||
map(
|
||||
ids =>
|
||||
new Map(
|
||||
ids.map(id => {
|
||||
const exists = this.pool.get(id);
|
||||
if (exists) {
|
||||
return [id, exists];
|
||||
}
|
||||
const record = this.framework.createEntity(DocRecord, { id });
|
||||
this.pool.set(id, record);
|
||||
return [id, record];
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
new Map()
|
||||
);
|
||||
|
||||
public readonly docs$ = this.docsMap$.selector(d => Array.from(d.values()));
|
||||
|
||||
public readonly trashDocs$ = LiveData.from<DocRecord[]>(
|
||||
this.store.watchTrashDocIds().pipe(
|
||||
map(ids =>
|
||||
ids.map(id => {
|
||||
const exists = this.pool.get(id);
|
||||
if (exists) {
|
||||
return exists;
|
||||
}
|
||||
const record = this.framework.createEntity(DocRecord, { id });
|
||||
this.pool.set(id, record);
|
||||
return record;
|
||||
})
|
||||
)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
public readonly isReady$ = LiveData.from(
|
||||
this.store.watchDocListReady(),
|
||||
false
|
||||
);
|
||||
|
||||
public doc$(id: string) {
|
||||
return this.docsMap$.selector(map => map.get(id));
|
||||
}
|
||||
|
||||
public setPrimaryMode(id: string, mode: DocMode) {
|
||||
return this.store.setDocPrimaryModeSetting(id, mode);
|
||||
}
|
||||
|
||||
public getPrimaryMode(id: string) {
|
||||
return this.store.getDocPrimaryModeSetting(id);
|
||||
}
|
||||
|
||||
public togglePrimaryMode(id: string) {
|
||||
const mode = (
|
||||
this.getPrimaryMode(id) === 'edgeless' ? 'page' : 'edgeless'
|
||||
) as DocMode;
|
||||
this.setPrimaryMode(id, mode);
|
||||
return this.getPrimaryMode(id);
|
||||
}
|
||||
|
||||
public primaryMode$(id: string) {
|
||||
return LiveData.from(
|
||||
this.store.watchDocPrimaryModeSetting(id),
|
||||
this.getPrimaryMode(id)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import type { DocProperties } from '../../db';
|
||||
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
|
||||
/**
|
||||
* # DocRecord
|
||||
*
|
||||
* Some data you can use without open a doc.
|
||||
*/
|
||||
export class DocRecord extends Entity<{ id: string }> {
|
||||
id: string = this.props.id;
|
||||
constructor(
|
||||
private readonly docsStore: DocsStore,
|
||||
private readonly docPropertiesStore: DocPropertiesStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
meta$ = LiveData.from<Partial<DocMeta>>(
|
||||
this.docsStore.watchDocMeta(this.id),
|
||||
{}
|
||||
);
|
||||
|
||||
properties$ = LiveData.from<DocProperties>(
|
||||
this.docPropertiesStore.watchDocProperties(this.id),
|
||||
{ id: this.id }
|
||||
);
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.properties$.selector(
|
||||
p => p['custom:' + propertyId]
|
||||
) as LiveData<string | undefined | null>;
|
||||
}
|
||||
|
||||
setCustomProperty(propertyId: string, value: string) {
|
||||
this.docPropertiesStore.updateDocProperties(this.id, {
|
||||
['custom:' + propertyId]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setProperty(propertyId: string, value: string) {
|
||||
this.docPropertiesStore.updateDocProperties(this.id, {
|
||||
[propertyId]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setMeta(meta: Partial<DocMeta>): void {
|
||||
this.docsStore.setDocMeta(this.id, meta);
|
||||
}
|
||||
|
||||
primaryMode$: LiveData<DocMode> = LiveData.from(
|
||||
this.docsStore.watchDocPrimaryModeSetting(this.id),
|
||||
'page' as DocMode
|
||||
).map(mode => (mode === 'edgeless' ? 'edgeless' : 'page') as DocMode);
|
||||
|
||||
setPrimaryMode(mode: DocMode) {
|
||||
return this.docsStore.setDocPrimaryModeSetting(this.id, mode);
|
||||
}
|
||||
|
||||
getPrimaryMode() {
|
||||
return this.docsStore.getDocPrimaryModeSetting(this.id);
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
return this.setMeta({ trash: true, trashDate: Date.now() });
|
||||
}
|
||||
|
||||
restoreFromTrash() {
|
||||
return this.setMeta({ trash: false, trashDate: undefined });
|
||||
}
|
||||
|
||||
title$ = this.meta$.map(meta => meta.title ?? '');
|
||||
|
||||
trash$ = this.meta$.map(meta => meta.trash ?? false);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { createEvent } from '../../../framework';
|
||||
import type { DocRecord } from '../entities/record';
|
||||
|
||||
export const DocCreated = createEvent<DocRecord>('DocCreated');
|
||||
@@ -1,34 +0,0 @@
|
||||
export { Doc } from './entities/doc';
|
||||
export { DocRecord } from './entities/record';
|
||||
export { DocRecordList } from './entities/record-list';
|
||||
export { DocCreated } from './events';
|
||||
export { DocScope } from './scopes/doc';
|
||||
export { DocService } from './services/doc';
|
||||
export { DocsService } from './services/docs';
|
||||
|
||||
import type { Framework } from '../../framework';
|
||||
import { WorkspaceDBService } from '../db';
|
||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||
import { Doc } from './entities/doc';
|
||||
import { DocPropertyList } from './entities/property-list';
|
||||
import { DocRecord } from './entities/record';
|
||||
import { DocRecordList } from './entities/record-list';
|
||||
import { DocScope } from './scopes/doc';
|
||||
import { DocService } from './services/doc';
|
||||
import { DocsService } from './services/docs';
|
||||
import { DocPropertiesStore } from './stores/doc-properties';
|
||||
import { DocsStore } from './stores/docs';
|
||||
|
||||
export function configureDocModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(DocsService, [DocsStore])
|
||||
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
||||
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
||||
.entity(DocRecord, [DocsStore, DocPropertiesStore])
|
||||
.entity(DocRecordList, [DocsStore])
|
||||
.entity(DocPropertyList, [DocPropertiesStore])
|
||||
.scope(DocScope)
|
||||
.entity(Doc, [DocScope, DocsStore, WorkspaceService])
|
||||
.service(DocService);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/affine/store';
|
||||
|
||||
import { Scope } from '../../../framework';
|
||||
import type { DocRecord } from '../entities/record';
|
||||
|
||||
export class DocScope extends Scope<{
|
||||
docId: string;
|
||||
record: DocRecord;
|
||||
blockSuiteDoc: BlockSuiteDoc;
|
||||
}> {}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { Doc } from '../entities/doc';
|
||||
|
||||
export class DocService extends Service {
|
||||
public readonly doc = this.framework.createEntity(Doc);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DeltaInsert } from '@blocksuite/affine/inline';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { type DocProps, initDocFromProps } from '../../../initialization';
|
||||
import { ObjectPool } from '../../../utils';
|
||||
import type { Doc } from '../entities/doc';
|
||||
import { DocPropertyList } from '../entities/property-list';
|
||||
import { DocRecordList } from '../entities/record-list';
|
||||
import { DocCreated } from '../events';
|
||||
import { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocService } from './doc';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
|
||||
export class DocsService extends Service {
|
||||
list = this.framework.createEntity(DocRecordList);
|
||||
|
||||
pool = new ObjectPool<string, Doc>({
|
||||
onDelete(obj) {
|
||||
obj.scope.dispose();
|
||||
},
|
||||
});
|
||||
|
||||
propertyList = this.framework.createEntity(DocPropertyList);
|
||||
|
||||
constructor(private readonly store: DocsStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
open(docId: string) {
|
||||
const docRecord = this.list.doc$(docId).value;
|
||||
if (!docRecord) {
|
||||
throw new Error('Doc record not found');
|
||||
}
|
||||
const blockSuiteDoc = this.store.getBlockSuiteDoc(docId);
|
||||
if (!blockSuiteDoc) {
|
||||
throw new Error('Doc not found');
|
||||
}
|
||||
|
||||
const exists = this.pool.get(docId);
|
||||
if (exists) {
|
||||
return { doc: exists.obj, release: exists.release };
|
||||
}
|
||||
|
||||
const docScope = this.framework.createScope(DocScope, {
|
||||
docId,
|
||||
blockSuiteDoc,
|
||||
record: docRecord,
|
||||
});
|
||||
|
||||
try {
|
||||
blockSuiteDoc.load();
|
||||
} catch (e) {
|
||||
logger.error('Failed to load doc', {
|
||||
docId,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = docScope.get(DocService).doc;
|
||||
|
||||
const { obj, release } = this.pool.put(docId, doc);
|
||||
|
||||
return { doc: obj, release };
|
||||
}
|
||||
|
||||
createDoc(
|
||||
options: {
|
||||
primaryMode?: DocMode;
|
||||
docProps?: DocProps;
|
||||
} = {}
|
||||
) {
|
||||
const doc = this.store.createBlockSuiteDoc();
|
||||
initDocFromProps(doc, options.docProps);
|
||||
this.store.markDocSyncStateAsReady(doc.id);
|
||||
const docRecord = this.list.doc$(doc.id).value;
|
||||
if (!docRecord) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
if (options.primaryMode) {
|
||||
docRecord.setPrimaryMode(options.primaryMode);
|
||||
}
|
||||
this.eventBus.emit(DocCreated, docRecord);
|
||||
return docRecord;
|
||||
}
|
||||
|
||||
async addLinkedDoc(targetDocId: string, linkedDocId: string) {
|
||||
const { doc, release } = this.open(targetDocId);
|
||||
doc.setPriorityLoad(10);
|
||||
await doc.waitForSyncReady();
|
||||
const text = new doc.blockSuiteDoc.Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: linkedDocId,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as DeltaInsert<AffineTextAttributes>[]);
|
||||
const [frame] = doc.blockSuiteDoc.getBlocksByFlavour('affine:note');
|
||||
frame &&
|
||||
doc.blockSuiteDoc.addBlock(
|
||||
'affine:paragraph' as never, // TODO(eyhn): fix type
|
||||
{ text },
|
||||
frame.id
|
||||
);
|
||||
release();
|
||||
}
|
||||
|
||||
async changeDocTitle(docId: string, newTitle: string) {
|
||||
const { doc, release } = this.open(docId);
|
||||
doc.setPriorityLoad(10);
|
||||
await doc.waitForSyncReady();
|
||||
doc.changeDocTitle(newTitle);
|
||||
release();
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
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';
|
||||
import { BUILT_IN_CUSTOM_PROPERTY_TYPE } from '../constants';
|
||||
|
||||
interface LegacyDocProperties {
|
||||
custom?: Record<string, { value: unknown } | undefined>;
|
||||
system?: Record<string, { value: unknown } | undefined>;
|
||||
}
|
||||
|
||||
type LegacyDocPropertyInfo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
icon?: 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 builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
|
||||
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
|
||||
const all = [
|
||||
...withLegacy,
|
||||
...differenceBy(builtIn, withLegacy, i => i.id),
|
||||
];
|
||||
return all.filter(i => !i.isDeleted);
|
||||
}
|
||||
|
||||
createDocPropertyInfo(
|
||||
config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
|
||||
) {
|
||||
return this.dbService.db.docCustomPropertyInfo.create(config);
|
||||
}
|
||||
|
||||
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);
|
||||
const isBuiltIn =
|
||||
needMigration && BUILT_IN_CUSTOM_PROPERTY_TYPE.some(i => i.id === id);
|
||||
if (isBuiltIn) {
|
||||
this.createPropertyFromBuiltIn(id, config);
|
||||
} else 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,
|
||||
});
|
||||
}
|
||||
|
||||
createPropertyFromBuiltIn(
|
||||
id: string,
|
||||
override: Partial<DocCustomPropertyInfo>
|
||||
) {
|
||||
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE.find(i => i.id === id);
|
||||
if (!builtIn) {
|
||||
return;
|
||||
}
|
||||
this.createDocPropertyInfo({ ...builtIn, ...override });
|
||||
}
|
||||
|
||||
watchDocPropertyInfoList() {
|
||||
return combineLatest([
|
||||
this.watchLegacyDocPropertyInfoList().pipe(
|
||||
map(this.upgradeLegacyDocPropertyInfoList)
|
||||
),
|
||||
this.dbService.db.docCustomPropertyInfo.find$(),
|
||||
]).pipe(
|
||||
map(([legacy, db]) => {
|
||||
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
|
||||
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
|
||||
const all = [
|
||||
...withLegacy,
|
||||
...differenceBy(builtIn, withLegacy, i => i.id),
|
||||
];
|
||||
return all.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, string> = {};
|
||||
for (const [key, info] of Object.entries(properties.system ?? {})) {
|
||||
if (info?.value !== undefined && info.value !== null) {
|
||||
newProperties[key] = info.value.toString();
|
||||
}
|
||||
}
|
||||
for (const [key, info] of Object.entries(properties.custom ?? {})) {
|
||||
if (info?.value !== undefined && info.value !== null) {
|
||||
newProperties['custom:' + key] = info.value.toString();
|
||||
}
|
||||
}
|
||||
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,
|
||||
icon: info.icon,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,140 +0,0 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs';
|
||||
import { Array as YArray, Map as YMap } from 'yjs';
|
||||
|
||||
import { Store } from '../../../framework';
|
||||
import { yjsObserve, yjsObserveByPath, yjsObserveDeep } from '../../../utils';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocPropertiesStore } from './doc-properties';
|
||||
|
||||
export class DocsStore extends Store {
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly docPropertiesStore: DocPropertiesStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getBlockSuiteDoc(id: string) {
|
||||
return this.workspaceService.workspace.docCollection.getDoc(id);
|
||||
}
|
||||
|
||||
createBlockSuiteDoc() {
|
||||
return this.workspaceService.workspace.docCollection.createDoc();
|
||||
}
|
||||
|
||||
watchDocIds() {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserve),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
return meta.map(v => v.get('id') as string);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchTrashDocIds() {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserveDeep),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
return meta
|
||||
.map(v => (v.get('trash') ? v.get('id') : null))
|
||||
.filter(Boolean) as string[];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchDocMeta(id: string) {
|
||||
let docMetaIndexCache = -1;
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserve),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
if (docMetaIndexCache >= 0) {
|
||||
const doc = meta.get(docMetaIndexCache);
|
||||
if (doc && doc.get('id') === id) {
|
||||
return doc as YMap<any>;
|
||||
}
|
||||
}
|
||||
|
||||
// meta is YArray, `for-of` is faster then `for`
|
||||
let i = 0;
|
||||
for (const doc of meta) {
|
||||
if (doc && doc.get('id') === id) {
|
||||
docMetaIndexCache = i;
|
||||
return doc as YMap<any>;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
switchMap(yjsObserveDeep),
|
||||
map(meta => {
|
||||
if (meta instanceof YMap) {
|
||||
return meta.toJSON() as Partial<DocMeta>;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchDocListReady() {
|
||||
return this.workspaceService.workspace.engine.rootDocState$
|
||||
.map(state => !state.syncing)
|
||||
.asObservable();
|
||||
}
|
||||
|
||||
setDocMeta(id: string, meta: Partial<DocMeta>) {
|
||||
this.workspaceService.workspace.docCollection.setDocMeta(id, meta);
|
||||
}
|
||||
|
||||
setDocPrimaryModeSetting(id: string, mode: DocMode) {
|
||||
return this.docPropertiesStore.updateDocProperties(id, {
|
||||
primaryMode: mode,
|
||||
});
|
||||
}
|
||||
|
||||
getDocPrimaryModeSetting(id: string) {
|
||||
return this.docPropertiesStore.getDocProperties(id)?.primaryMode;
|
||||
}
|
||||
|
||||
watchDocPrimaryModeSetting(id: string) {
|
||||
return this.docPropertiesStore.watchDocProperties(id).pipe(
|
||||
map(config => config?.primaryMode),
|
||||
distinctUntilChanged((p, c) => p === c)
|
||||
);
|
||||
}
|
||||
|
||||
waitForDocLoadReady(id: string) {
|
||||
return this.workspaceService.workspace.engine.doc.waitForReady(id);
|
||||
}
|
||||
|
||||
setPriorityLoad(id: string, priority: number) {
|
||||
return this.workspaceService.workspace.engine.doc.setPriority(id, priority);
|
||||
}
|
||||
|
||||
markDocSyncStateAsReady(id: string) {
|
||||
this.workspaceService.workspace.engine.doc.markAsReady(id);
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import type { FlagInfo } from './types';
|
||||
|
||||
const isNotStableBuild = BUILD_CONFIG.appBuildType !== 'stable';
|
||||
const isDesktopEnvironment = BUILD_CONFIG.isElectron;
|
||||
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
|
||||
const isMobile = BUILD_CONFIG.isMobileEdition;
|
||||
|
||||
export const AFFINE_FLAGS = {
|
||||
enable_ai: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai.description',
|
||||
hide: true,
|
||||
configurable: true,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_new_dnd: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_new_dnd',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-new-dnd.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-new-dnd.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_database_full_width: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_database_full_width',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-full-width.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-full-width.description',
|
||||
configurable: isCanaryBuild,
|
||||
},
|
||||
enable_database_attachment_note: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_database_attachment_note',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.description',
|
||||
configurable: isNotStableBuild,
|
||||
},
|
||||
enable_block_query: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_block_query',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-block-query.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-block-query.description',
|
||||
configurable: isCanaryBuild,
|
||||
},
|
||||
enable_synced_doc_block: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_synced_doc_block',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_edgeless_text: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_edgeless_text',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_color_picker: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_color_picker',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-color-picker.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-color-picker.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_ai_chat_block: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_ai_chat_block',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_ai_onboarding: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_ai_onboarding',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_mind_map_import: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mind_map_import',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_multi_view: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multi-view.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multi-view.description',
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280009690004324405',
|
||||
configurable: isDesktopEnvironment,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
enable_emoji_folder_icon: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.description',
|
||||
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280014319865696351/1280014319865696351',
|
||||
configurable: true,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_emoji_doc_icon: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.description',
|
||||
feedbackType: 'discord',
|
||||
feedbackLink:
|
||||
'https://discord.com/channels/959027316334407691/1280014319865696351',
|
||||
configurable: true,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_editor_settings: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-editor-settings.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-editor-settings.description',
|
||||
configurable: false,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_offline_mode: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-offline-mode.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-offline-mode.description',
|
||||
configurable: isDesktopEnvironment,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_theme_editor: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-theme-editor.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-theme-editor.description',
|
||||
configurable: isCanaryBuild && !isMobile,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
enable_local_workspace: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-local-workspace.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-local-workspace.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: isDesktopEnvironment || isCanaryBuild,
|
||||
},
|
||||
enable_advanced_block_visibility: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_advanced_block_visibility',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.description',
|
||||
configurable: true,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_mobile_keyboard_toolbar: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mobile_keyboard_toolbar',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.description',
|
||||
configurable: false,
|
||||
defaultState: isMobile,
|
||||
},
|
||||
enable_mobile_linked_doc_menu: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_mobile_linked_doc_menu',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.description',
|
||||
configurable: false,
|
||||
defaultState: isMobile,
|
||||
},
|
||||
enable_multiple_cloud_servers: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description',
|
||||
configurable: isDesktopEnvironment,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_mobile_edgeless_editing: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.description',
|
||||
configurable: isMobile,
|
||||
defaultState: false,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { NEVER } from 'rxjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import type { GlobalStateService } from '../../storage';
|
||||
import { AFFINE_FLAGS } from '../constant';
|
||||
import type { FlagInfo } from '../types';
|
||||
|
||||
const FLAG_PREFIX = 'affine-flag:';
|
||||
|
||||
export type Flag<F extends FlagInfo = FlagInfo> = {
|
||||
readonly value: F['defaultState'] extends boolean
|
||||
? boolean
|
||||
: boolean | undefined;
|
||||
set: (value: boolean) => void;
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
$: F['defaultState'] extends boolean
|
||||
? LiveData<boolean>
|
||||
: LiveData<boolean> | LiveData<boolean | undefined>;
|
||||
} & F;
|
||||
|
||||
export class Flags extends Entity {
|
||||
private readonly globalState = this.globalStateService.globalState;
|
||||
|
||||
constructor(private readonly globalStateService: GlobalStateService) {
|
||||
super();
|
||||
|
||||
Object.entries(AFFINE_FLAGS).forEach(([flagKey, flag]) => {
|
||||
const configurable = flag.configurable ?? true;
|
||||
const defaultState =
|
||||
'defaultState' in flag ? flag.defaultState : undefined;
|
||||
const getValue = () => {
|
||||
return configurable
|
||||
? (this.globalState.get<boolean>(FLAG_PREFIX + flagKey) ??
|
||||
defaultState)
|
||||
: defaultState;
|
||||
};
|
||||
const item = {
|
||||
...flag,
|
||||
get value() {
|
||||
return getValue();
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (!configurable) {
|
||||
return;
|
||||
}
|
||||
this.globalState.set(FLAG_PREFIX + flagKey, value);
|
||||
},
|
||||
$: configurable
|
||||
? LiveData.from<boolean | undefined>(
|
||||
this.globalState.watch<boolean>(FLAG_PREFIX + flagKey),
|
||||
undefined
|
||||
).map(value => value ?? defaultState)
|
||||
: LiveData.from(NEVER, defaultState),
|
||||
} as Flag<typeof flag>;
|
||||
Object.defineProperty(this, flagKey, {
|
||||
get: () => {
|
||||
return item;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type FlagsExt = Flags & {
|
||||
[K in keyof AFFINE_FLAGS]: Flag<AFFINE_FLAGS[K]>;
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Framework } from '../../framework';
|
||||
import { GlobalStateService } from '../storage';
|
||||
import { Flags } from './entities/flags';
|
||||
import { FeatureFlagService } from './services/feature-flag';
|
||||
|
||||
export { AFFINE_FLAGS } from './constant';
|
||||
export type { Flag } from './entities/flags';
|
||||
export { FeatureFlagService } from './services/feature-flag';
|
||||
export type { FlagInfo } from './types';
|
||||
|
||||
export function configureFeatureFlagModule(framework: Framework) {
|
||||
framework.service(FeatureFlagService).entity(Flags, [GlobalStateService]);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { distinctUntilChanged, skip } from 'rxjs';
|
||||
|
||||
import { OnEvent, Service } from '../../../framework';
|
||||
import { ApplicationStarted } from '../../lifecycle';
|
||||
import type { Workspace } from '../../workspace';
|
||||
import { WorkspaceInitialized } from '../../workspace/events';
|
||||
import { AFFINE_FLAGS } from '../constant';
|
||||
import { Flags, type FlagsExt } from '../entities/flags';
|
||||
|
||||
@OnEvent(WorkspaceInitialized, e => e.setupBlocksuiteEditorFlags)
|
||||
@OnEvent(ApplicationStarted, e => e.setupRestartListener)
|
||||
export class FeatureFlagService extends Service {
|
||||
flags = this.framework.createEntity(Flags) as FlagsExt;
|
||||
|
||||
setupBlocksuiteEditorFlags(workspace: Workspace) {
|
||||
for (const [key, flag] of Object.entries(AFFINE_FLAGS)) {
|
||||
if (flag.category === 'blocksuite') {
|
||||
const value = this.flags[key as keyof AFFINE_FLAGS].value;
|
||||
if (value !== undefined) {
|
||||
workspace.docCollection.awarenessStore.setFlag(flag.bsFlag, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupRestartListener() {
|
||||
this.flags.enable_ai.$.pipe(distinctUntilChanged(), skip(1)).subscribe(
|
||||
() => {
|
||||
// when enable_ai flag changes, reload the page.
|
||||
window.location.reload();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { BlockSuiteFlags } from '@blocksuite/affine/global/types';
|
||||
|
||||
type FeedbackType = 'discord' | 'email' | 'github';
|
||||
|
||||
export type FlagInfo = {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
configurable?: boolean;
|
||||
defaultState?: boolean; // default to open and not controlled by user
|
||||
/**
|
||||
* hide in the feature flag settings, but still can be controlled by the code
|
||||
*/
|
||||
hide?: boolean;
|
||||
feedbackType?: FeedbackType;
|
||||
feedbackLink?: string;
|
||||
} & (
|
||||
| {
|
||||
category: 'affine';
|
||||
}
|
||||
| {
|
||||
category: 'blocksuite';
|
||||
bsFlag: keyof BlockSuiteFlags;
|
||||
}
|
||||
);
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import { MemoryMemento } from '../../../storage';
|
||||
|
||||
export class GlobalContext extends Entity {
|
||||
memento = new MemoryMemento();
|
||||
|
||||
workspaceId = this.define<string>('workspaceId');
|
||||
workspaceFlavour = this.define<string>('workspaceFlavour');
|
||||
|
||||
serverId = this.define<string>('serverId');
|
||||
|
||||
/**
|
||||
* is in doc page
|
||||
*/
|
||||
isDoc = this.define<boolean>('isDoc');
|
||||
isTrashDoc = this.define<boolean>('isTrashDoc');
|
||||
docId = this.define<string>('docId');
|
||||
docMode = this.define<DocMode>('docMode');
|
||||
|
||||
/**
|
||||
* is in collection page
|
||||
*/
|
||||
isCollection = this.define<boolean>('isCollection');
|
||||
collectionId = this.define<string>('collectionId');
|
||||
|
||||
/**
|
||||
* is in trash page
|
||||
*/
|
||||
isTrash = this.define<boolean>('isTrash');
|
||||
|
||||
/**
|
||||
* is in tag page
|
||||
*/
|
||||
isTag = this.define<boolean>('isTag');
|
||||
tagId = this.define<string>('tagId');
|
||||
|
||||
/**
|
||||
* is in all docs page
|
||||
*/
|
||||
isAllDocs = this.define<boolean>('isAllDocs');
|
||||
|
||||
define<T>(key: string) {
|
||||
this.memento.set(key, null);
|
||||
const livedata$ = LiveData.from(this.memento.watch<T>(key), null);
|
||||
return {
|
||||
get: () => this.memento.get(key) as T | null,
|
||||
set: (value: T | null) => this.memento.set(key, value),
|
||||
$: livedata$,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export { GlobalContextService } from './services/global-context';
|
||||
|
||||
import type { Framework } from '../../framework';
|
||||
import { GlobalContext } from './entities/global-context';
|
||||
import { GlobalContextService } from './services/global-context';
|
||||
|
||||
export function configureGlobalContextModule(framework: Framework) {
|
||||
framework.service(GlobalContextService).entity(GlobalContext);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { GlobalContext } from '../entities/global-context';
|
||||
|
||||
export class GlobalContextService extends Service {
|
||||
globalContext = this.framework.createEntity(GlobalContext);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { Framework } from '../../framework';
|
||||
import { LifecycleService } from './service/lifecycle';
|
||||
|
||||
export {
|
||||
ApplicationFocused,
|
||||
ApplicationStarted,
|
||||
LifecycleService,
|
||||
} from './service/lifecycle';
|
||||
|
||||
export function configureLifecycleModule(framework: Framework) {
|
||||
framework.service(LifecycleService);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { createEvent, Service } from '../../../framework';
|
||||
|
||||
/**
|
||||
* Event that is emitted when application is started.
|
||||
*/
|
||||
export const ApplicationStarted = createEvent<boolean>('ApplicationStartup');
|
||||
|
||||
/**
|
||||
* Event that is emitted when browser tab or windows is focused again, after being blurred.
|
||||
* Can be used to actively refresh some data.
|
||||
*/
|
||||
export const ApplicationFocused = createEvent<boolean>('ApplicationFocused');
|
||||
|
||||
export class LifecycleService extends Service {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
applicationStart() {
|
||||
this.eventBus.emit(ApplicationStarted, true);
|
||||
}
|
||||
|
||||
applicationFocus() {
|
||||
this.eventBus.emit(ApplicationFocused, true);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
export {
|
||||
GlobalCache,
|
||||
GlobalSessionState,
|
||||
GlobalState,
|
||||
} from './providers/global';
|
||||
export {
|
||||
GlobalCacheService,
|
||||
GlobalSessionStateService,
|
||||
GlobalStateService,
|
||||
} from './services/global';
|
||||
|
||||
import type { Framework } from '../../framework';
|
||||
import { MemoryMemento } from '../../storage';
|
||||
import {
|
||||
GlobalCache,
|
||||
GlobalSessionState,
|
||||
GlobalState,
|
||||
} from './providers/global';
|
||||
import {
|
||||
GlobalCacheService,
|
||||
GlobalSessionStateService,
|
||||
GlobalStateService,
|
||||
} from './services/global';
|
||||
|
||||
export const configureGlobalStorageModule = (framework: Framework) => {
|
||||
framework.service(GlobalStateService, [GlobalState]);
|
||||
framework.service(GlobalCacheService, [GlobalCache]);
|
||||
framework.service(GlobalSessionStateService, [GlobalSessionState]);
|
||||
};
|
||||
|
||||
export const configureTestingGlobalStorage = (framework: Framework) => {
|
||||
framework.impl(GlobalCache, MemoryMemento);
|
||||
framework.impl(GlobalState, MemoryMemento);
|
||||
framework.impl(GlobalSessionState, MemoryMemento);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { createIdentifier } from '../../../framework';
|
||||
import type { Memento } from '../../../storage';
|
||||
|
||||
/**
|
||||
* A memento object that stores the entire application state.
|
||||
*
|
||||
* State is persisted, even the application is closed.
|
||||
*/
|
||||
export interface GlobalState extends Memento {}
|
||||
|
||||
export const GlobalState = createIdentifier<GlobalState>('GlobalState');
|
||||
|
||||
/**
|
||||
* A memento object that stores the entire application cache.
|
||||
*
|
||||
* Cache may be deleted from time to time, business logic should not rely on cache.
|
||||
*/
|
||||
export interface GlobalCache extends Memento {}
|
||||
export const GlobalCache = createIdentifier<GlobalCache>('GlobalCache');
|
||||
|
||||
/**
|
||||
* A memento object that stores session state.
|
||||
*
|
||||
* Session state is not persisted, it will be cleared when the application is closed. (thinking about sessionStorage)
|
||||
*/
|
||||
export interface GlobalSessionState extends Memento {}
|
||||
export const GlobalSessionState =
|
||||
createIdentifier<GlobalSessionState>('GlobalSessionState');
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import type {
|
||||
GlobalCache,
|
||||
GlobalSessionState,
|
||||
GlobalState,
|
||||
} from '../providers/global';
|
||||
|
||||
export class GlobalStateService extends Service {
|
||||
constructor(public readonly globalState: GlobalState) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalCacheService extends Service {
|
||||
constructor(public readonly globalCache: GlobalCache) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalSessionStateService extends Service {
|
||||
constructor(public readonly globalSessionState: GlobalSessionState) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { Framework } from '../../../framework';
|
||||
import { configureTestingGlobalStorage } from '../../storage';
|
||||
import {
|
||||
configureTestingWorkspaceProvider,
|
||||
configureWorkspaceModule,
|
||||
Workspace,
|
||||
WorkspacesService,
|
||||
} from '..';
|
||||
|
||||
describe('Workspace System', () => {
|
||||
test('create workspace', async () => {
|
||||
const framework = new Framework();
|
||||
configureTestingGlobalStorage(framework);
|
||||
configureWorkspaceModule(framework);
|
||||
configureTestingWorkspaceProvider(framework);
|
||||
|
||||
const provider = framework.provider();
|
||||
const workspaceService = provider.get(WorkspacesService);
|
||||
expect(workspaceService.list.workspaces$.value.length).toBe(0);
|
||||
|
||||
const workspace = workspaceService.open({
|
||||
metadata: await workspaceService.create('local'),
|
||||
});
|
||||
|
||||
expect(workspace.workspace).toBeInstanceOf(Workspace);
|
||||
|
||||
expect(workspaceService.list.workspaces$.value.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { AwarenessEngine, BlobEngine, DocEngine } from '../../../sync';
|
||||
import { throwIfAborted } from '../../../utils';
|
||||
import { WorkspaceEngineBeforeStart } from '../events';
|
||||
import type { WorkspaceEngineProvider } from '../providers/flavour';
|
||||
import type { WorkspaceService } from '../services/workspace';
|
||||
|
||||
export class WorkspaceEngine extends Entity<{
|
||||
engineProvider: WorkspaceEngineProvider;
|
||||
}> {
|
||||
doc = new DocEngine(
|
||||
this.props.engineProvider.getDocStorage(),
|
||||
this.props.engineProvider.getDocServer()
|
||||
);
|
||||
|
||||
blob = new BlobEngine(
|
||||
this.props.engineProvider.getLocalBlobStorage(),
|
||||
this.props.engineProvider.getRemoteBlobStorages()
|
||||
);
|
||||
|
||||
awareness = new AwarenessEngine(
|
||||
this.props.engineProvider.getAwarenessConnections()
|
||||
);
|
||||
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
}
|
||||
|
||||
setRootDoc(yDoc: YDoc) {
|
||||
this.doc.setPriority(yDoc.guid, 100);
|
||||
this.doc.addDoc(yDoc);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.eventBus.emit(WorkspaceEngineBeforeStart, this);
|
||||
this.doc.start();
|
||||
this.awareness.connect(this.workspaceService.workspace.awareness);
|
||||
if (!BUILD_CONFIG.isMobileEdition) {
|
||||
// currently, blob synchronization consumes a lot of memory and is temporarily disabled on mobile devices.
|
||||
this.blob.start();
|
||||
}
|
||||
}
|
||||
|
||||
canGracefulStop() {
|
||||
return this.doc.engineState$.value.saving === 0;
|
||||
}
|
||||
|
||||
async waitForGracefulStop(abort?: AbortSignal) {
|
||||
await this.doc.waitForSaved();
|
||||
throwIfAborted(abort);
|
||||
this.forceStop();
|
||||
}
|
||||
|
||||
forceStop() {
|
||||
this.doc.stop();
|
||||
this.awareness.disconnect();
|
||||
this.blob.stop();
|
||||
}
|
||||
|
||||
docEngineState$ = this.doc.engineState$;
|
||||
|
||||
rootDocState$ = this.doc.docState$(this.workspaceService.workspace.id);
|
||||
|
||||
waitForDocSynced() {
|
||||
return this.doc.waitForSynced();
|
||||
}
|
||||
|
||||
waitForRootDocReady() {
|
||||
return this.doc.waitForReady(this.workspaceService.workspace.id);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.forceStop();
|
||||
this.doc.dispose();
|
||||
this.awareness.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { combineLatest, map, of, switchMap } from 'rxjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type { WorkspaceFlavoursService } from '../services/flavours';
|
||||
|
||||
export class WorkspaceList extends Entity {
|
||||
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
|
||||
this.flavoursService.flavours$.pipe(
|
||||
switchMap(flavours =>
|
||||
combineLatest(flavours.map(flavour => flavour.workspaces$)).pipe(
|
||||
map(workspaces => workspaces.flat())
|
||||
)
|
||||
)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
isRevalidating$ = LiveData.from<boolean>(
|
||||
this.flavoursService.flavours$.pipe(
|
||||
switchMap(flavours =>
|
||||
combineLatest(
|
||||
flavours.map(flavour => flavour.isRevalidating$ ?? of(false))
|
||||
).pipe(map(isLoadings => isLoadings.some(isLoading => isLoading)))
|
||||
)
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
workspace$(id: string) {
|
||||
return this.workspaces$.map(workspaces =>
|
||||
workspaces.find(workspace => workspace.id === id)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||
super();
|
||||
}
|
||||
|
||||
revalidate() {
|
||||
this.flavoursService.flavours$.value.forEach(provider => {
|
||||
provider.revalidate?.();
|
||||
});
|
||||
}
|
||||
|
||||
waitForRevalidation(signal?: AbortSignal) {
|
||||
this.revalidate();
|
||||
return this.isRevalidating$.waitFor(isLoading => !isLoading, signal);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import {
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '../../../livedata';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
||||
import type { WorkspaceFlavoursService } from '../services/flavours';
|
||||
import type { WorkspaceProfileCacheStore } from '../stores/profile-cache';
|
||||
import type { Workspace } from './workspace';
|
||||
|
||||
const logger = new DebugLogger('affine:workspace-profile');
|
||||
|
||||
export interface WorkspaceProfileInfo {
|
||||
avatar?: string;
|
||||
name?: string;
|
||||
isOwner?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isTeam?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* # WorkspaceProfile
|
||||
*
|
||||
* This class take care of workspace avatar and name
|
||||
*/
|
||||
export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> {
|
||||
private readonly provider: WorkspaceFlavourProvider | null;
|
||||
|
||||
get id() {
|
||||
return this.props.metadata.id;
|
||||
}
|
||||
|
||||
profile$ = LiveData.from<WorkspaceProfileInfo | null>(
|
||||
this.cache.watchProfileCache(this.props.metadata.id),
|
||||
null
|
||||
);
|
||||
|
||||
avatar$ = this.profile$.map(v => v?.avatar);
|
||||
name$ = this.profile$.map(v => v?.name);
|
||||
|
||||
isLoading$ = new LiveData(false);
|
||||
|
||||
constructor(
|
||||
private readonly cache: WorkspaceProfileCacheStore,
|
||||
flavoursService: WorkspaceFlavoursService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.provider =
|
||||
flavoursService.flavours$.value.find(
|
||||
p => p.flavour === this.props.metadata.flavour
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
private setProfile(info: WorkspaceProfileInfo) {
|
||||
if (isEqual(this.profile$.value, info)) {
|
||||
return;
|
||||
}
|
||||
this.cache.setProfileCache(this.props.metadata.id, info);
|
||||
}
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMap(() => {
|
||||
const provider = this.provider;
|
||||
if (!provider) {
|
||||
return EMPTY;
|
||||
}
|
||||
return fromPromise(signal =>
|
||||
provider.getWorkspaceProfile(this.props.metadata.id, signal)
|
||||
).pipe(
|
||||
mergeMap(info => {
|
||||
if (info) {
|
||||
this.setProfile({ ...this.profile$.value, ...info });
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
catchError(err => {
|
||||
logger.error(err);
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => this.isLoading$.next(true)),
|
||||
onComplete(() => this.isLoading$.next(false))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
syncWithWorkspace(workspace: Workspace) {
|
||||
workspace.name$.subscribe(name => {
|
||||
const old = this.profile$.value;
|
||||
this.setProfile({ ...old, name: name ?? old?.name });
|
||||
});
|
||||
workspace.avatar$.subscribe(avatar => {
|
||||
const old = this.profile$.value;
|
||||
this.setProfile({ ...old, avatar: avatar ?? old?.avatar });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { DocCollection } from '@blocksuite/affine/store';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { Awareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import { Entity } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import { WorkspaceDBService } from '../../db';
|
||||
import { getAFFiNEWorkspaceSchema } from '../global-schema';
|
||||
import type { WorkspaceScope } from '../scopes/workspace';
|
||||
import { WorkspaceEngineService } from '../services/engine';
|
||||
|
||||
export class Workspace extends Entity {
|
||||
constructor(public readonly scope: WorkspaceScope) {
|
||||
super();
|
||||
}
|
||||
|
||||
readonly id = this.scope.props.openOptions.metadata.id;
|
||||
|
||||
readonly openOptions = this.scope.props.openOptions;
|
||||
|
||||
readonly meta = this.scope.props.openOptions.metadata;
|
||||
|
||||
readonly flavour = this.meta.flavour;
|
||||
|
||||
_docCollection: DocCollection | null = null;
|
||||
|
||||
get docCollection() {
|
||||
if (!this._docCollection) {
|
||||
this._docCollection = new DocCollection({
|
||||
id: this.openOptions.metadata.id,
|
||||
blobSources: {
|
||||
main: this.engine.blob,
|
||||
},
|
||||
idGenerator: () => nanoid(),
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
});
|
||||
this._docCollection.slots.docCreated.on(id => {
|
||||
this.engine.doc.markAsReady(id);
|
||||
});
|
||||
}
|
||||
return this._docCollection;
|
||||
}
|
||||
|
||||
get db() {
|
||||
return this.framework.get(WorkspaceDBService).db;
|
||||
}
|
||||
|
||||
get awareness() {
|
||||
return this.docCollection.awarenessStore.awareness as Awareness;
|
||||
}
|
||||
|
||||
get rootYDoc() {
|
||||
return this.docCollection.doc;
|
||||
}
|
||||
|
||||
get canGracefulStop() {
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
|
||||
get engine() {
|
||||
return this.framework.get(WorkspaceEngineService).engine;
|
||||
}
|
||||
|
||||
name$ = LiveData.from<string | undefined>(
|
||||
new Observable(subscriber => {
|
||||
subscriber.next(this.docCollection.meta.name);
|
||||
return this.docCollection.meta.commonFieldsUpdated.on(() => {
|
||||
subscriber.next(this.docCollection.meta.name);
|
||||
}).dispose;
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
avatar$ = LiveData.from<string | undefined>(
|
||||
new Observable(subscriber => {
|
||||
subscriber.next(this.docCollection.meta.avatar);
|
||||
return this.docCollection.meta.commonFieldsUpdated.on(() => {
|
||||
subscriber.next(this.docCollection.meta.avatar);
|
||||
}).dispose;
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
override dispose(): void {
|
||||
this.docCollection.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createEvent } from '../../../framework';
|
||||
import type { WorkspaceEngine } from '../entities/engine';
|
||||
import type { Workspace } from '../entities/workspace';
|
||||
|
||||
export const WorkspaceEngineBeforeStart = createEvent<WorkspaceEngine>(
|
||||
'WorkspaceEngineBeforeStart'
|
||||
);
|
||||
|
||||
export const WorkspaceInitialized = createEvent<Workspace>(
|
||||
'WorkspaceInitialized'
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AffineSchemas } from '@blocksuite/affine/blocks/schemas';
|
||||
import { Schema } from '@blocksuite/affine/store';
|
||||
|
||||
import { AIChatBlockSchema } from '../../blocksuite/blocks/ai-chat-block/ai-chat-model';
|
||||
|
||||
let _schema: Schema | null = null;
|
||||
export function getAFFiNEWorkspaceSchema() {
|
||||
if (!_schema) {
|
||||
_schema = new Schema();
|
||||
|
||||
_schema.register([...AffineSchemas, AIChatBlockSchema]);
|
||||
}
|
||||
|
||||
return _schema;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { type Memento, wrapMemento } from '../../../storage';
|
||||
import type { GlobalCache, GlobalState } from '../../storage';
|
||||
import type {
|
||||
WorkspaceLocalCache,
|
||||
WorkspaceLocalState,
|
||||
} from '../providers/storage';
|
||||
import type { WorkspaceService } from '../services/workspace';
|
||||
|
||||
export class WorkspaceLocalStateImpl implements WorkspaceLocalState {
|
||||
wrapped: Memento;
|
||||
constructor(workspaceService: WorkspaceService, globalState: GlobalState) {
|
||||
this.wrapped = wrapMemento(
|
||||
globalState,
|
||||
`workspace-state:${workspaceService.workspace.id}:`
|
||||
);
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
return this.wrapped.keys();
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.wrapped.get<T>(key);
|
||||
}
|
||||
|
||||
watch<T>(key: string) {
|
||||
return this.wrapped.watch<T>(key);
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
return this.wrapped.set<T>(key, value);
|
||||
}
|
||||
|
||||
del(key: string): void {
|
||||
return this.wrapped.del(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
return this.wrapped.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceLocalCacheImpl implements WorkspaceLocalCache {
|
||||
wrapped: Memento;
|
||||
constructor(workspaceService: WorkspaceService, globalCache: GlobalCache) {
|
||||
this.wrapped = wrapMemento(
|
||||
globalCache,
|
||||
`workspace-cache:${workspaceService.workspace.id}:`
|
||||
);
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
return this.wrapped.keys();
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.wrapped.get<T>(key);
|
||||
}
|
||||
|
||||
watch<T>(key: string) {
|
||||
return this.wrapped.watch<T>(key);
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
return this.wrapped.set<T>(key, value);
|
||||
}
|
||||
|
||||
del(key: string): void {
|
||||
return this.wrapped.del(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
return this.wrapped.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
export type { WorkspaceProfileInfo } from './entities/profile';
|
||||
export { Workspace } from './entities/workspace';
|
||||
export { WorkspaceEngineBeforeStart, WorkspaceInitialized } from './events';
|
||||
export { getAFFiNEWorkspaceSchema } from './global-schema';
|
||||
export type { WorkspaceMetadata } from './metadata';
|
||||
export type { WorkspaceOpenOptions } from './open-options';
|
||||
export type { WorkspaceEngineProvider } from './providers/flavour';
|
||||
export type { WorkspaceFlavourProvider } from './providers/flavour';
|
||||
export { WorkspaceFlavoursProvider } from './providers/flavour';
|
||||
export { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
||||
export { WorkspaceScope } from './scopes/workspace';
|
||||
export { WorkspaceService } from './services/workspace';
|
||||
export { WorkspacesService } from './services/workspaces';
|
||||
|
||||
import type { Framework } from '../../framework';
|
||||
import { GlobalCache, GlobalState } from '../storage';
|
||||
import { WorkspaceEngine } from './entities/engine';
|
||||
import { WorkspaceList } from './entities/list';
|
||||
import { WorkspaceProfile } from './entities/profile';
|
||||
import { Workspace } from './entities/workspace';
|
||||
import {
|
||||
WorkspaceLocalCacheImpl,
|
||||
WorkspaceLocalStateImpl,
|
||||
} from './impls/storage';
|
||||
import { WorkspaceFlavoursProvider } from './providers/flavour';
|
||||
import { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
||||
import { WorkspaceScope } from './scopes/workspace';
|
||||
import { WorkspaceDestroyService } from './services/destroy';
|
||||
import { WorkspaceEngineService } from './services/engine';
|
||||
import { WorkspaceFactoryService } from './services/factory';
|
||||
import { WorkspaceFlavoursService } from './services/flavours';
|
||||
import { WorkspaceListService } from './services/list';
|
||||
import { WorkspaceProfileService } from './services/profile';
|
||||
import { WorkspaceRepositoryService } from './services/repo';
|
||||
import { WorkspaceTransformService } from './services/transform';
|
||||
import { WorkspaceService } from './services/workspace';
|
||||
import { WorkspacesService } from './services/workspaces';
|
||||
import { WorkspaceProfileCacheStore } from './stores/profile-cache';
|
||||
import { TestingWorkspaceFlavoursProvider } from './testing/testing-provider';
|
||||
|
||||
export function configureWorkspaceModule(framework: Framework) {
|
||||
framework
|
||||
.service(WorkspacesService, [
|
||||
WorkspaceFlavoursService,
|
||||
WorkspaceListService,
|
||||
WorkspaceProfileService,
|
||||
WorkspaceTransformService,
|
||||
WorkspaceRepositoryService,
|
||||
WorkspaceFactoryService,
|
||||
WorkspaceDestroyService,
|
||||
])
|
||||
.service(WorkspaceFlavoursService, [[WorkspaceFlavoursProvider]])
|
||||
.service(WorkspaceDestroyService, [WorkspaceFlavoursService])
|
||||
.service(WorkspaceListService)
|
||||
.entity(WorkspaceList, [WorkspaceFlavoursService])
|
||||
.service(WorkspaceProfileService)
|
||||
.store(WorkspaceProfileCacheStore, [GlobalCache])
|
||||
.entity(WorkspaceProfile, [
|
||||
WorkspaceProfileCacheStore,
|
||||
WorkspaceFlavoursService,
|
||||
])
|
||||
.service(WorkspaceFactoryService, [WorkspaceFlavoursService])
|
||||
.service(WorkspaceTransformService, [
|
||||
WorkspaceFactoryService,
|
||||
WorkspaceDestroyService,
|
||||
])
|
||||
.service(WorkspaceRepositoryService, [
|
||||
WorkspaceFlavoursService,
|
||||
WorkspaceProfileService,
|
||||
])
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceService)
|
||||
.entity(Workspace, [WorkspaceScope])
|
||||
.service(WorkspaceEngineService, [WorkspaceScope])
|
||||
.entity(WorkspaceEngine, [WorkspaceService])
|
||||
.impl(WorkspaceLocalState, WorkspaceLocalStateImpl, [
|
||||
WorkspaceService,
|
||||
GlobalState,
|
||||
])
|
||||
.impl(WorkspaceLocalCache, WorkspaceLocalCacheImpl, [
|
||||
WorkspaceService,
|
||||
GlobalCache,
|
||||
]);
|
||||
}
|
||||
|
||||
export function configureTestingWorkspaceProvider(framework: Framework) {
|
||||
framework.impl(
|
||||
WorkspaceFlavoursProvider('LOCAL'),
|
||||
TestingWorkspaceFlavoursProvider,
|
||||
[GlobalState]
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export type WorkspaceMetadata = {
|
||||
id: string;
|
||||
flavour: string;
|
||||
initialized?: boolean;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { WorkspaceMetadata } from './metadata';
|
||||
|
||||
export interface WorkspaceOpenOptions {
|
||||
metadata: WorkspaceMetadata;
|
||||
isSharedMode?: boolean;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
|
||||
import { createIdentifier } from '../../../framework';
|
||||
import type { LiveData } from '../../../livedata';
|
||||
import type {
|
||||
AwarenessConnection,
|
||||
BlobStorage,
|
||||
DocServer,
|
||||
DocStorage,
|
||||
} from '../../../sync';
|
||||
import type { WorkspaceProfileInfo } from '../entities/profile';
|
||||
import type { Workspace } from '../entities/workspace';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
|
||||
export interface WorkspaceEngineProvider {
|
||||
getDocServer(): DocServer | null;
|
||||
getDocStorage(): DocStorage;
|
||||
getLocalBlobStorage(): BlobStorage;
|
||||
getRemoteBlobStorages(): BlobStorage[];
|
||||
getAwarenessConnections(): AwarenessConnection[];
|
||||
}
|
||||
|
||||
export interface WorkspaceFlavourProvider {
|
||||
flavour: string;
|
||||
|
||||
deleteWorkspace(id: string): Promise<void>;
|
||||
|
||||
createWorkspace(
|
||||
initial: (
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage,
|
||||
docStorage: DocStorage
|
||||
) => Promise<void>
|
||||
): Promise<WorkspaceMetadata>;
|
||||
|
||||
workspaces$: LiveData<WorkspaceMetadata[]>;
|
||||
|
||||
/**
|
||||
* means the workspace list is loading. if it's true, the workspace page will show loading spinner.
|
||||
*/
|
||||
isRevalidating$?: LiveData<boolean>;
|
||||
|
||||
/**
|
||||
* revalidate the workspace list.
|
||||
*
|
||||
* will be called when user open workspace list, or workspace not found.
|
||||
*/
|
||||
revalidate?: () => void;
|
||||
|
||||
getWorkspaceProfile(
|
||||
id: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<WorkspaceProfileInfo | undefined>;
|
||||
|
||||
getWorkspaceBlob(id: string, blob: string): Promise<Blob | null>;
|
||||
|
||||
getEngineProvider(workspaceId: string): WorkspaceEngineProvider;
|
||||
|
||||
onWorkspaceInitialized?(workspace: Workspace): void;
|
||||
}
|
||||
|
||||
export interface WorkspaceFlavoursProvider {
|
||||
workspaceFlavours$: LiveData<WorkspaceFlavourProvider[]>;
|
||||
}
|
||||
|
||||
export const WorkspaceFlavoursProvider =
|
||||
createIdentifier<WorkspaceFlavoursProvider>('WorkspaceFlavoursProvider');
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createIdentifier } from '../../../framework';
|
||||
import type { Memento } from '../../../storage';
|
||||
|
||||
export interface WorkspaceLocalState extends Memento {}
|
||||
export interface WorkspaceLocalCache extends Memento {}
|
||||
|
||||
export const WorkspaceLocalState = createIdentifier<WorkspaceLocalState>(
|
||||
'WorkspaceLocalState'
|
||||
);
|
||||
|
||||
export const WorkspaceLocalCache = createIdentifier<WorkspaceLocalCache>(
|
||||
'WorkspaceLocalCache'
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Scope } from '../../../framework';
|
||||
import type { WorkspaceOpenOptions } from '../open-options';
|
||||
import type { WorkspaceEngineProvider } from '../providers/flavour';
|
||||
|
||||
export type { DocCollection } from '@blocksuite/affine/store';
|
||||
|
||||
export class WorkspaceScope extends Scope<{
|
||||
openOptions: WorkspaceOpenOptions;
|
||||
engineProvider: WorkspaceEngineProvider;
|
||||
}> {}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type { WorkspaceFlavoursService } from './flavours';
|
||||
|
||||
export class WorkspaceDestroyService extends Service {
|
||||
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||
super();
|
||||
}
|
||||
|
||||
deleteWorkspace = async (metadata: WorkspaceMetadata) => {
|
||||
const provider = this.flavoursService.flavours$.value.find(
|
||||
p => p.flavour === metadata.flavour
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
||||
}
|
||||
return provider.deleteWorkspace(metadata.id);
|
||||
};
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { WorkspaceEngine } from '../entities/engine';
|
||||
import type { WorkspaceScope } from '../scopes/workspace';
|
||||
|
||||
export class WorkspaceEngineService extends Service {
|
||||
private _engine: WorkspaceEngine | null = null;
|
||||
get engine() {
|
||||
if (!this._engine) {
|
||||
this._engine = this.framework.createEntity(WorkspaceEngine, {
|
||||
engineProvider: this.workspaceScope.props.engineProvider,
|
||||
});
|
||||
}
|
||||
return this._engine;
|
||||
}
|
||||
|
||||
constructor(private readonly workspaceScope: WorkspaceScope) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._engine?.dispose();
|
||||
this._engine = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import type { BlobStorage, DocStorage } from '../../../sync';
|
||||
import type { WorkspaceFlavoursService } from './flavours';
|
||||
|
||||
export class WorkspaceFactoryService extends Service {
|
||||
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* create workspace
|
||||
* @param flavour workspace flavour
|
||||
* @param initial callback to put initial data to workspace
|
||||
* @returns workspace id
|
||||
*/
|
||||
create = async (
|
||||
flavour: string,
|
||||
initial: (
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage,
|
||||
docStorage: DocStorage
|
||||
) => Promise<void> = () => Promise.resolve()
|
||||
) => {
|
||||
const provider = this.flavoursService.flavours$.value.find(
|
||||
x => x.flavour === flavour
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown workspace flavour: ${flavour}`);
|
||||
}
|
||||
const metadata = await provider.createWorkspace(initial);
|
||||
return metadata;
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import type { WorkspaceFlavoursProvider } from '../providers/flavour';
|
||||
|
||||
export class WorkspaceFlavoursService extends Service {
|
||||
constructor(private readonly providers: WorkspaceFlavoursProvider[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
flavours$ = LiveData.from(
|
||||
combineLatest(this.providers.map(p => p.workspaceFlavours$)).pipe(
|
||||
map(flavours => flavours.flat())
|
||||
),
|
||||
[]
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { WorkspaceList } from '../entities/list';
|
||||
|
||||
export class WorkspaceListService extends Service {
|
||||
list = this.framework.createEntity(WorkspaceList);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { ObjectPool } from '../../../utils';
|
||||
import { WorkspaceProfile } from '../entities/profile';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
|
||||
export class WorkspaceProfileService extends Service {
|
||||
pool = new ObjectPool<string, WorkspaceProfile>();
|
||||
|
||||
getProfile = (metadata: WorkspaceMetadata): WorkspaceProfile => {
|
||||
const exists = this.pool.get(metadata.id);
|
||||
if (exists) {
|
||||
return exists.obj;
|
||||
}
|
||||
|
||||
const profile = this.framework.createEntity(WorkspaceProfile, {
|
||||
metadata,
|
||||
});
|
||||
|
||||
return this.pool.put(metadata.id, profile).obj;
|
||||
};
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { ObjectPool } from '../../../utils';
|
||||
import type { Workspace } from '../entities/workspace';
|
||||
import { WorkspaceInitialized } from '../events';
|
||||
import type { WorkspaceOpenOptions } from '../open-options';
|
||||
import type { WorkspaceEngineProvider } from '../providers/flavour';
|
||||
import { WorkspaceScope } from '../scopes/workspace';
|
||||
import type { WorkspaceFlavoursService } from './flavours';
|
||||
import type { WorkspaceProfileService } from './profile';
|
||||
import { WorkspaceService } from './workspace';
|
||||
|
||||
const logger = new DebugLogger('affine:workspace-repository');
|
||||
|
||||
export class WorkspaceRepositoryService extends Service {
|
||||
constructor(
|
||||
private readonly flavoursService: WorkspaceFlavoursService,
|
||||
private readonly profileRepo: WorkspaceProfileService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
pool = new ObjectPool<string, Workspace>({
|
||||
onDelete(workspace) {
|
||||
workspace.scope.dispose();
|
||||
},
|
||||
onDangling(workspace) {
|
||||
return workspace.canGracefulStop;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* open workspace reference by metadata.
|
||||
*
|
||||
* You basically don't need to call this function directly, use the react hook `useWorkspace(metadata)` instead.
|
||||
*
|
||||
* @returns the workspace reference and a release function, don't forget to call release function when you don't
|
||||
* need the workspace anymore.
|
||||
*/
|
||||
open = (
|
||||
options: WorkspaceOpenOptions,
|
||||
customProvider?: WorkspaceEngineProvider
|
||||
): {
|
||||
workspace: Workspace;
|
||||
dispose: () => void;
|
||||
} => {
|
||||
if (options.isSharedMode) {
|
||||
const workspace = this.instantiate(options, customProvider);
|
||||
return {
|
||||
workspace,
|
||||
dispose: () => {
|
||||
workspace.scope.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const exist = this.pool.get(options.metadata.id);
|
||||
if (exist) {
|
||||
return {
|
||||
workspace: exist.obj,
|
||||
dispose: exist.release,
|
||||
};
|
||||
}
|
||||
|
||||
const workspace = this.instantiate(options, customProvider);
|
||||
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
|
||||
// this.list.getInformation(metadata).syncWithWorkspace(workspace);
|
||||
|
||||
const ref = this.pool.put(workspace.meta.id, workspace);
|
||||
|
||||
return {
|
||||
workspace: ref.obj,
|
||||
dispose: ref.release,
|
||||
};
|
||||
};
|
||||
|
||||
instantiate(
|
||||
openOptions: WorkspaceOpenOptions,
|
||||
customProvider?: WorkspaceEngineProvider
|
||||
) {
|
||||
logger.info(
|
||||
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
|
||||
);
|
||||
const flavourProvider = this.flavoursService.flavours$.value.find(
|
||||
p => p.flavour === openOptions.metadata.flavour
|
||||
);
|
||||
const provider =
|
||||
customProvider ??
|
||||
flavourProvider?.getEngineProvider(openOptions.metadata.id);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`Unknown workspace flavour: ${openOptions.metadata.flavour}`
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceScope = this.framework.createScope(WorkspaceScope, {
|
||||
openOptions,
|
||||
engineProvider: provider,
|
||||
});
|
||||
|
||||
const workspace = workspaceScope.get(WorkspaceService).workspace;
|
||||
|
||||
workspace.engine.setRootDoc(workspace.docCollection.doc);
|
||||
workspace.engine.start();
|
||||
|
||||
this.framework.emitEvent(WorkspaceInitialized, workspace);
|
||||
|
||||
flavourProvider?.onWorkspaceInitialized?.(workspace);
|
||||
|
||||
this.profileRepo
|
||||
.getProfile(openOptions.metadata)
|
||||
.syncWithWorkspace(workspace);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { assertEquals } from '@blocksuite/affine/global/utils';
|
||||
import { applyUpdate } from 'yjs';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { transformWorkspaceDBLocalToCloud } from '../../db';
|
||||
import type { Workspace } from '../entities/workspace';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type { WorkspaceDestroyService } from './destroy';
|
||||
import type { WorkspaceFactoryService } from './factory';
|
||||
|
||||
export class WorkspaceTransformService extends Service {
|
||||
constructor(
|
||||
private readonly factory: WorkspaceFactoryService,
|
||||
private readonly destroy: WorkspaceDestroyService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to transform local workspace to cloud workspace
|
||||
*
|
||||
* @param accountId - all local user data will be transformed to this account
|
||||
*/
|
||||
transformLocalToCloud = async (
|
||||
local: Workspace,
|
||||
accountId: string,
|
||||
flavour: string
|
||||
): Promise<WorkspaceMetadata> => {
|
||||
assertEquals(local.flavour, 'local');
|
||||
|
||||
const localDocStorage = local.engine.doc.storage.behavior;
|
||||
|
||||
const newMetadata = await this.factory.create(
|
||||
flavour,
|
||||
async (docCollection, blobStorage, docStorage) => {
|
||||
const rootDocBinary = await localDocStorage.doc.get(
|
||||
local.docCollection.doc.guid
|
||||
);
|
||||
|
||||
if (rootDocBinary) {
|
||||
applyUpdate(docCollection.doc, rootDocBinary);
|
||||
}
|
||||
|
||||
for (const subdoc of docCollection.doc.getSubdocs()) {
|
||||
const subdocBinary = await localDocStorage.doc.get(subdoc.guid);
|
||||
if (subdocBinary) {
|
||||
applyUpdate(subdoc, subdocBinary);
|
||||
}
|
||||
}
|
||||
|
||||
// transform db
|
||||
await transformWorkspaceDBLocalToCloud(
|
||||
local.id,
|
||||
docCollection.id,
|
||||
localDocStorage,
|
||||
docStorage,
|
||||
accountId
|
||||
);
|
||||
|
||||
const blobList = await local.engine.blob.list();
|
||||
|
||||
for (const blobKey of blobList) {
|
||||
const blob = await local.engine.blob.get(blobKey);
|
||||
if (blob) {
|
||||
await blobStorage.set(blobKey, blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await this.destroy.deleteWorkspace(local.meta);
|
||||
|
||||
return newMetadata;
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import { Workspace } from '../entities/workspace';
|
||||
|
||||
export class WorkspaceService extends Service {
|
||||
_workspace: Workspace | null = null;
|
||||
|
||||
get workspace() {
|
||||
if (!this._workspace) {
|
||||
this._workspace = this.framework.createEntity(Workspace);
|
||||
}
|
||||
return this._workspace;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._workspace?.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Service } from '../../../framework';
|
||||
import type { WorkspaceMetadata } from '..';
|
||||
import type { WorkspaceDestroyService } from './destroy';
|
||||
import type { WorkspaceFactoryService } from './factory';
|
||||
import type { WorkspaceFlavoursService } from './flavours';
|
||||
import type { WorkspaceListService } from './list';
|
||||
import type { WorkspaceProfileService } from './profile';
|
||||
import type { WorkspaceRepositoryService } from './repo';
|
||||
import type { WorkspaceTransformService } from './transform';
|
||||
|
||||
export class WorkspacesService extends Service {
|
||||
get list() {
|
||||
return this.listService.list;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly flavoursService: WorkspaceFlavoursService,
|
||||
private readonly listService: WorkspaceListService,
|
||||
private readonly profileRepo: WorkspaceProfileService,
|
||||
private readonly transform: WorkspaceTransformService,
|
||||
private readonly workspaceRepo: WorkspaceRepositoryService,
|
||||
private readonly workspaceFactory: WorkspaceFactoryService,
|
||||
private readonly destroy: WorkspaceDestroyService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get deleteWorkspace() {
|
||||
return this.destroy.deleteWorkspace;
|
||||
}
|
||||
|
||||
get getProfile() {
|
||||
return this.profileRepo.getProfile;
|
||||
}
|
||||
|
||||
get transformLocalToCloud() {
|
||||
return this.transform.transformLocalToCloud;
|
||||
}
|
||||
|
||||
get open() {
|
||||
return this.workspaceRepo.open;
|
||||
}
|
||||
|
||||
get create() {
|
||||
return this.workspaceFactory.create;
|
||||
}
|
||||
|
||||
async getWorkspaceBlob(meta: WorkspaceMetadata, blob: string) {
|
||||
return await this.flavoursService.flavours$.value
|
||||
.find(x => x.flavour === meta.flavour)
|
||||
?.getWorkspaceBlob(meta.id, blob);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import { Store } from '../../../framework';
|
||||
import type { GlobalCache } from '../../storage';
|
||||
import type { WorkspaceProfileInfo } from '../entities/profile';
|
||||
|
||||
const WORKSPACE_PROFILE_CACHE_KEY = 'workspace-information:';
|
||||
|
||||
export class WorkspaceProfileCacheStore extends Store {
|
||||
constructor(private readonly cache: GlobalCache) {
|
||||
super();
|
||||
}
|
||||
|
||||
watchProfileCache(workspaceId: string) {
|
||||
return this.cache.watch(WORKSPACE_PROFILE_CACHE_KEY + workspaceId).pipe(
|
||||
map(data => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = data as WorkspaceProfileInfo;
|
||||
return {
|
||||
avatar: info.avatar,
|
||||
name: info.name,
|
||||
isOwner: info.isOwner,
|
||||
isAdmin: info.isAdmin,
|
||||
isTeam: info.isTeam,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setProfileCache(workspaceId: string, info: WorkspaceProfileInfo) {
|
||||
this.cache.set(WORKSPACE_PROFILE_CACHE_KEY + workspaceId, info);
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { DocCollection, nanoid } from '@blocksuite/affine/store';
|
||||
import { map } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { Service } from '../../../framework';
|
||||
import { LiveData } from '../../../livedata';
|
||||
import { wrapMemento } from '../../../storage';
|
||||
import {
|
||||
type BlobStorage,
|
||||
type DocStorage,
|
||||
MemoryDocStorage,
|
||||
} from '../../../sync';
|
||||
import { MemoryBlobStorage } from '../../../sync/blob/blob';
|
||||
import type { GlobalState } from '../../storage';
|
||||
import type { WorkspaceProfileInfo } from '../entities/profile';
|
||||
import { getAFFiNEWorkspaceSchema } from '../global-schema';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type {
|
||||
WorkspaceEngineProvider,
|
||||
WorkspaceFlavourProvider,
|
||||
WorkspaceFlavoursProvider,
|
||||
} from '../providers/flavour';
|
||||
|
||||
class TestingWorkspaceLocalProvider implements WorkspaceFlavourProvider {
|
||||
flavour = 'local';
|
||||
|
||||
store = wrapMemento(this.globalStore, 'testing/');
|
||||
workspaceListStore = wrapMemento(this.store, 'workspaces/');
|
||||
docStorage = new MemoryDocStorage(wrapMemento(this.store, 'docs/'));
|
||||
|
||||
constructor(private readonly globalStore: GlobalState) {}
|
||||
|
||||
async deleteWorkspace(id: string): Promise<void> {
|
||||
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
||||
const newList = list.filter(meta => meta.id !== id);
|
||||
this.workspaceListStore.set('list', newList);
|
||||
}
|
||||
async createWorkspace(
|
||||
initial: (
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage,
|
||||
docStorage: DocStorage
|
||||
) => Promise<void>
|
||||
): Promise<WorkspaceMetadata> {
|
||||
const id = nanoid();
|
||||
const meta = { id, flavour: 'local' };
|
||||
|
||||
const blobStorage = new MemoryBlobStorage(
|
||||
wrapMemento(this.store, id + '/blobs/')
|
||||
);
|
||||
|
||||
const docCollection = new DocCollection({
|
||||
id: id,
|
||||
idGenerator: () => nanoid(),
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
blobSources: {
|
||||
main: blobStorage,
|
||||
},
|
||||
});
|
||||
|
||||
// apply initial state
|
||||
await initial(docCollection, blobStorage, this.docStorage);
|
||||
|
||||
// save workspace to storage
|
||||
await this.docStorage.doc.set(id, encodeStateAsUpdate(docCollection.doc));
|
||||
for (const subdocs of docCollection.doc.getSubdocs()) {
|
||||
await this.docStorage.doc.set(subdocs.guid, encodeStateAsUpdate(subdocs));
|
||||
}
|
||||
|
||||
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
||||
this.workspaceListStore.set('list', [...list, meta]);
|
||||
|
||||
docCollection.dispose();
|
||||
|
||||
return { id, flavour: 'local' };
|
||||
}
|
||||
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
|
||||
this.workspaceListStore
|
||||
.watch<WorkspaceMetadata[]>('list')
|
||||
.pipe(map(m => m ?? [])),
|
||||
[]
|
||||
);
|
||||
async getWorkspaceProfile(
|
||||
id: string
|
||||
): Promise<WorkspaceProfileInfo | undefined> {
|
||||
const data = await this.docStorage.doc.get(id);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bs = new DocCollection({
|
||||
id,
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
});
|
||||
|
||||
applyUpdate(bs.doc, data);
|
||||
|
||||
bs.dispose();
|
||||
|
||||
return {
|
||||
name: bs.meta.name,
|
||||
avatar: bs.meta.avatar,
|
||||
isOwner: true,
|
||||
};
|
||||
}
|
||||
getWorkspaceBlob(id: string, blob: string): Promise<Blob | null> {
|
||||
return new MemoryBlobStorage(wrapMemento(this.store, id + '/blobs/')).get(
|
||||
blob
|
||||
);
|
||||
}
|
||||
getEngineProvider(workspaceId: string): WorkspaceEngineProvider {
|
||||
return {
|
||||
getDocStorage: () => {
|
||||
return this.docStorage;
|
||||
},
|
||||
getAwarenessConnections() {
|
||||
return [];
|
||||
},
|
||||
getDocServer() {
|
||||
return null;
|
||||
},
|
||||
getLocalBlobStorage: () => {
|
||||
return new MemoryBlobStorage(
|
||||
wrapMemento(this.store, workspaceId + '/blobs/')
|
||||
);
|
||||
},
|
||||
getRemoteBlobStorages() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TestingWorkspaceFlavoursProvider
|
||||
extends Service
|
||||
implements WorkspaceFlavoursProvider
|
||||
{
|
||||
constructor(private readonly globalStore: GlobalState) {
|
||||
super();
|
||||
}
|
||||
workspaceFlavours$ = new LiveData<WorkspaceFlavourProvider[]>([
|
||||
new TestingWorkspaceLocalProvider(this.globalStore),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user