feat(core): doc level awareness (#10646)

This commit is contained in:
EYHN
2025-03-06 06:05:45 +00:00
parent 2b30d756e2
commit 5c8b81581c
14 changed files with 65 additions and 79 deletions

View File

@@ -5,7 +5,7 @@ import { multiPlayersColor } from './color-picker';
export class RemoteColorManager { export class RemoteColorManager {
private get awarenessStore() { private get awarenessStore() {
return this.std.store.workspace.awarenessStore; return this.std.store.awarenessStore;
} }
constructor(readonly std: BlockStdScope) { constructor(readonly std: BlockStdScope) {

View File

@@ -4,7 +4,6 @@ import type { Awareness } from 'y-protocols/awareness.js';
import type * as Y from 'yjs'; import type * as Y from 'yjs';
import type { IdGenerator } from '../utils/id-generator.js'; import type { IdGenerator } from '../utils/id-generator.js';
import type { AwarenessStore } from '../yjs/awareness.js';
import type { CreateBlocksOptions, Doc, GetBlocksOptions } from './doc.js'; import type { CreateBlocksOptions, Doc, GetBlocksOptions } from './doc.js';
import type { Store } from './store/store.js'; import type { Store } from './store/store.js';
import type { WorkspaceMeta } from './workspace-meta.js'; import type { WorkspaceMeta } from './workspace-meta.js';
@@ -14,7 +13,6 @@ export interface Workspace {
readonly meta: WorkspaceMeta; readonly meta: WorkspaceMeta;
readonly idGenerator: IdGenerator; readonly idGenerator: IdGenerator;
readonly blobSync: BlobEngine; readonly blobSync: BlobEngine;
readonly awarenessStore: AwarenessStore;
readonly onLoadDoc?: (doc: Y.Doc) => void; readonly onLoadDoc?: (doc: Y.Doc) => void;
readonly onLoadAwareness?: (awareness: Awareness) => void; readonly onLoadAwareness?: (awareness: Awareness) => void;

View File

@@ -1,49 +0,0 @@
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react';
import { AuthService } from '../../../modules/cloud';
const SyncAwarenessInnerLoggedIn = () => {
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const currentWorkspace = useService(WorkspaceService).workspace;
useEffect(() => {
if (account && currentWorkspace) {
currentWorkspace.docCollection.awarenessStore.awareness.setLocalStateField(
'user',
{
name: account.label,
// TODO(@eyhn): add avatar?
}
);
return () => {
currentWorkspace.docCollection.awarenessStore.awareness.setLocalStateField(
'user',
null
);
};
}
return;
}, [currentWorkspace, account]);
return null;
};
const SyncAwarenessInner = () => {
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
if (loginStatus === 'authenticated') {
return <SyncAwarenessInnerLoggedIn />;
}
return null;
};
// TODO(@eyhn): we could do something more interesting here, e.g., show where the current user is
export const SyncAwareness = () => {
return <SyncAwarenessInner />;
};

View File

@@ -8,7 +8,6 @@ import {
CopilotClient, CopilotClient,
setupAIProvider, setupAIProvider,
} from '@affine/core/blocksuite/ai'; } from '@affine/core/blocksuite/ai';
import { SyncAwareness } from '@affine/core/components/affine/awareness';
import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands'; import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands';
import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands'; import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands';
import { OverCapacityNotification } from '@affine/core/components/over-capacity'; import { OverCapacityNotification } from '@affine/core/components/over-capacity';
@@ -176,7 +175,6 @@ export const WorkspaceSideEffects = () => {
return ( return (
<> <>
<QuickSearchContainer /> <QuickSearchContainer />
<SyncAwareness />
<OverCapacityNotification /> <OverCapacityNotification />
</> </>
); );

View File

@@ -50,7 +50,7 @@ const useLoadDoc = (pageId: string) => {
if (doc && isInTrash) { if (doc && isInTrash) {
doc.blockSuiteDoc.readonly = true; doc.blockSuiteDoc.readonly = true;
} }
}, [currentWorkspace.docCollection.awarenessStore, doc, isInTrash]); }, [doc, isInTrash]);
return { return {
doc, doc,

View File

@@ -39,6 +39,7 @@ import { type Framework } from '@toeverything/infra';
import { DocScope } from '../doc/scopes/doc'; import { DocScope } from '../doc/scopes/doc';
import { DocService } from '../doc/services/doc'; import { DocService } from '../doc/services/doc';
import { EditorScope } from '../editor';
import { GlobalCache, GlobalState } from '../storage/providers/global'; import { GlobalCache, GlobalState } from '../storage/providers/global';
import { GlobalStateService } from '../storage/services/global'; import { GlobalStateService } from '../storage/services/global';
import { UrlService } from '../url'; import { UrlService } from '../url';
@@ -63,6 +64,7 @@ import { AuthService } from './services/auth';
import { CaptchaService } from './services/captcha'; import { CaptchaService } from './services/captcha';
import { CloudDocMetaService } from './services/cloud-doc-meta'; import { CloudDocMetaService } from './services/cloud-doc-meta';
import { DefaultServerService } from './services/default-server'; import { DefaultServerService } from './services/default-server';
import { EditorUserCursorLabelService } from './services/editor-user-cursor-label';
import { EventSourceService } from './services/eventsource'; import { EventSourceService } from './services/eventsource';
import { FetchService } from './services/fetch'; import { FetchService } from './services/fetch';
import { GraphQLService } from './services/graphql'; import { GraphQLService } from './services/graphql';
@@ -168,4 +170,10 @@ export function configureCloudModule(framework: Framework) {
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService]) .entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService])
.service(SelfhostLicenseService, [SelfhostLicenseStore, WorkspaceService]) .service(SelfhostLicenseService, [SelfhostLicenseStore, WorkspaceService])
.store(SelfhostLicenseStore, [WorkspaceServerService]); .store(SelfhostLicenseStore, [WorkspaceServerService]);
framework
.scope(WorkspaceScope)
.scope(DocScope)
.scope(EditorScope)
.service(EditorUserCursorLabelService, [WorkspaceServerService]);
} }

View File

@@ -0,0 +1,27 @@
import { OnEvent, Service } from '@toeverything/infra';
import type { Editor } from '../../editor';
import { EditorInitialized } from '../../editor/events';
import type { WorkspaceServerService } from './workspace-server';
@OnEvent(EditorInitialized, i => i.onEditorInitialized)
export class EditorUserCursorLabelService extends Service {
constructor(private readonly workspaceServerService: WorkspaceServerService) {
super();
}
onEditorInitialized(editor: Editor) {
if (this.workspaceServerService.server) {
const subscription =
this.workspaceServerService.server.account$.subscribe(account => {
editor.doc.blockSuiteDoc.awarenessStore.awareness.setLocalStateField(
'user',
{
name: account?.label,
}
);
});
this.disposables.push(() => subscription.unsubscribe());
}
}
}

View File

@@ -1,5 +1,8 @@
import { createEvent } from '@toeverything/infra'; import { createEvent } from '@toeverything/infra';
import type { Doc } from '../entities/doc';
import type { DocRecord } from '../entities/record'; import type { DocRecord } from '../entities/record';
export const DocCreated = createEvent<DocRecord>('DocCreated'); export const DocCreated = createEvent<DocRecord>('DocCreated');
export const DocInitialized = createEvent<Doc>('DocInitialized');

View File

@@ -20,7 +20,7 @@ import { getAFFiNEWorkspaceSchema } from '../../workspace';
import type { Doc } from '../entities/doc'; import type { Doc } from '../entities/doc';
import { DocPropertyList } from '../entities/property-list'; import { DocPropertyList } from '../entities/property-list';
import { DocRecordList } from '../entities/record-list'; import { DocRecordList } from '../entities/record-list';
import { DocCreated } from '../events'; import { DocCreated, DocInitialized } from '../events';
import { DocScope } from '../scopes/doc'; import { DocScope } from '../scopes/doc';
import type { DocPropertiesStore } from '../stores/doc-properties'; import type { DocPropertiesStore } from '../stores/doc-properties';
import type { DocsStore } from '../stores/docs'; import type { DocsStore } from '../stores/docs';
@@ -105,6 +105,8 @@ export class DocsService extends Service {
const doc = docScope.get(DocService).doc; const doc = docScope.get(DocService).doc;
doc.scope.emitEvent(DocInitialized, doc);
const { obj, release } = this.pool.put(docId, doc); const { obj, release } = this.pool.put(docId, doc);
return { doc: obj, release }; return { doc: obj, release };

View File

@@ -0,0 +1,5 @@
import { createEvent } from '@toeverything/infra';
import type { Editor } from '../entities/editor';
export const EditorInitialized = createEvent<Editor>('EditorInitialized');

View File

@@ -1,9 +1,12 @@
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import { Editor } from '../entities/editor'; import { Editor } from '../entities/editor';
import { EditorInitialized } from '../events';
export class EditorsService extends Service { export class EditorsService extends Service {
createEditor() { createEditor() {
return this.framework.createEntity(Editor); const editor = this.framework.createEntity(Editor);
editor.scope.emitEvent(EditorInitialized, editor);
return editor;
} }
} }

View File

@@ -1,7 +1,6 @@
import type { Workspace as WorkspaceInterface } from '@blocksuite/affine/store'; import type { Workspace as WorkspaceInterface } from '@blocksuite/affine/store';
import { Entity, LiveData } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import type { Awareness } from 'y-protocols/awareness.js';
import { WorkspaceImpl } from '../impls/workspace'; import { WorkspaceImpl } from '../impls/workspace';
import type { WorkspaceScope } from '../scopes/workspace'; import type { WorkspaceScope } from '../scopes/workspace';
@@ -58,10 +57,6 @@ export class Workspace extends Entity {
return this._docCollection; return this._docCollection;
} }
get awareness() {
return this.docCollection.awarenessStore.awareness as Awareness;
}
get rootYDoc() { get rootYDoc() {
return this.docCollection.doc; return this.docCollection.doc;
} }

View File

@@ -1,7 +1,7 @@
import { SpecProvider } from '@blocksuite/affine/blocks'; import { SpecProvider } from '@blocksuite/affine/blocks';
import { Slot } from '@blocksuite/affine/global/utils'; import { Slot } from '@blocksuite/affine/global/utils';
import { import {
type AwarenessStore, AwarenessStore,
type Doc, type Doc,
type GetBlocksOptions, type GetBlocksOptions,
type Query, type Query,
@@ -10,13 +10,13 @@ import {
type YBlock, type YBlock,
} from '@blocksuite/affine/store'; } from '@blocksuite/affine/store';
import { signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
type DocOptions = { type DocOptions = {
id: string; id: string;
collection: Workspace; collection: Workspace;
doc: Y.Doc; doc: Y.Doc;
awarenessStore: AwarenessStore;
}; };
export class DocImpl implements Doc { export class DocImpl implements Doc {
@@ -155,12 +155,11 @@ export class DocImpl implements Doc {
return this._yBlocks; return this._yBlocks;
} }
constructor({ id, collection, doc, awarenessStore }: DocOptions) { constructor({ id, collection, doc }: DocOptions) {
this.id = id; this.id = id;
this.rootDoc = doc; this.rootDoc = doc;
this.awarenessStore = awarenessStore;
this._ySpaceDoc = this._initSubDoc() as Y.Doc; this._ySpaceDoc = this._initSubDoc() as Y.Doc;
this.awarenessStore = new AwarenessStore(new Awareness(this._ySpaceDoc));
this._yBlocks = this._ySpaceDoc.getMap('blocks'); this._yBlocks = this._ySpaceDoc.getMap('blocks');
this._collection = collection; this._collection = collection;
@@ -228,12 +227,14 @@ export class DocImpl implements Doc {
} }
private _destroy() { private _destroy() {
this.awarenessStore.destroy();
this._ySpaceDoc.destroy(); this._ySpaceDoc.destroy();
this._onLoadSlot.dispose(); this._onLoadSlot.dispose();
this._loaded = false; this._loaded = false;
} }
dispose() { dispose() {
this._destroy();
this.slots.historyUpdated.dispose(); this.slots.historyUpdated.dispose();
if (this.ready) { if (this.ready) {
@@ -303,6 +304,7 @@ export class DocImpl implements Doc {
this.spaceDoc.load(); this.spaceDoc.load();
this.workspace.onLoadDoc?.(this.spaceDoc); this.workspace.onLoadDoc?.(this.spaceDoc);
this.workspace.onLoadAwareness?.(this.awarenessStore.awareness);
this._initYBlocks(); this._initYBlocks();

View File

@@ -4,7 +4,6 @@ import {
} from '@blocksuite/affine/global/exceptions'; } from '@blocksuite/affine/global/exceptions';
import { NoopLogger, Slot } from '@blocksuite/affine/global/utils'; import { NoopLogger, Slot } from '@blocksuite/affine/global/utils';
import { import {
AwarenessStore,
type CreateBlocksOptions, type CreateBlocksOptions,
type Doc, type Doc,
type GetBlocksOptions, type GetBlocksOptions,
@@ -19,7 +18,7 @@ import {
type BlobSource, type BlobSource,
MemoryBlobSource, MemoryBlobSource,
} from '@blocksuite/affine/sync'; } from '@blocksuite/affine/sync';
import { Awareness } from 'y-protocols/awareness.js'; import type { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { DocImpl } from './doc'; import { DocImpl } from './doc';
@@ -33,8 +32,6 @@ type WorkspaceOptions = {
}; };
export class WorkspaceImpl implements Workspace { export class WorkspaceImpl implements Workspace {
readonly awarenessStore: AwarenessStore;
readonly blobSync: BlobEngine; readonly blobSync: BlobEngine;
readonly blockCollections = new Map<string, Doc>(); readonly blockCollections = new Map<string, Doc>();
@@ -68,11 +65,9 @@ export class WorkspaceImpl implements Workspace {
}: WorkspaceOptions = {}) { }: WorkspaceOptions = {}) {
this.id = id || ''; this.id = id || '';
this.doc = new Y.Doc({ guid: id }); this.doc = new Y.Doc({ guid: id });
this.awarenessStore = new AwarenessStore(new Awareness(this.doc));
this.onLoadDoc = onLoadDoc; this.onLoadDoc = onLoadDoc;
this.onLoadAwareness = onLoadAwareness;
this.onLoadDoc?.(this.doc); this.onLoadDoc?.(this.doc);
this.onLoadAwareness?.(this.awarenessStore.awareness); this.onLoadAwareness = onLoadAwareness;
blobSource = blobSource ?? new MemoryBlobSource(); blobSource = blobSource ?? new MemoryBlobSource();
const logger = new NoopLogger(); const logger = new NoopLogger();
@@ -91,7 +86,6 @@ export class WorkspaceImpl implements Workspace {
id: docId, id: docId,
collection: this, collection: this,
doc: this.doc, doc: this.doc,
awarenessStore: this.awarenessStore,
}); });
this.blockCollections.set(doc.id, doc); this.blockCollections.set(doc.id, doc);
}); });
@@ -139,10 +133,6 @@ export class WorkspaceImpl implements Workspace {
}) as Store; }) as Store;
} }
dispose() {
this.awarenessStore.destroy();
}
private _getDoc(docId: string): Doc | null { private _getDoc(docId: string): Doc | null {
const space = this.docs.get(docId) as Doc | undefined; const space = this.docs.get(docId) as Doc | undefined;
return space ?? null; return space ?? null;
@@ -172,4 +162,8 @@ export class WorkspaceImpl implements Workspace {
this.meta.removeDocMeta(docId); this.meta.removeDocMeta(docId);
this.blockCollections.delete(docId); this.blockCollections.delete(docId);
} }
dispose() {
this.blockCollections.forEach(doc => doc.dispose());
}
} }