feat(editor): feature flag store extension builder (#12235)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced feature flag synchronization for enhanced control over feature availability.
  - Added new configuration options for store management, allowing initialization and feature flag setup.

- **Improvements**
  - Updated how store extensions are accessed throughout the app for more robust initialization and configuration.
  - Enhanced workspace entities to support feature flag services, improving flexibility for workspace-specific features.
  - Centralized configuration of storage implementations for Electron environments.

- **Refactor**
  - Simplified editor module by removing direct feature flag synchronization logic.
  - Streamlined imports and configuration for storage modules, especially in Electron-based apps.

- **Bug Fixes**
  - Ensured consistent retrieval of store extensions across various modules and platforms.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-12 09:29:14 +00:00
parent ef3775e8a9
commit bc00a58ae1
23 changed files with 178 additions and 68 deletions

View File

@@ -5,10 +5,7 @@ import { DefaultTool } from '@blocksuite/affine/blocks/surface';
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
import type { DocMode, ReferenceParams } from '@blocksuite/affine/model';
import { HighlightSelection } from '@blocksuite/affine/shared/selection';
import {
DocModeProvider,
FeatureFlagService as BSFeatureFlagService,
} from '@blocksuite/affine/shared/services';
import { DocModeProvider } from '@blocksuite/affine/shared/services';
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx';
import type { InlineEditor } from '@blocksuite/std/inline';
import { effect } from '@preact/signals-core';
@@ -17,7 +14,6 @@ import { defaults, isEqual, omit } from 'lodash-es';
import { skip } from 'rxjs';
import type { DocService } from '../../doc';
import { AFFINE_FLAGS, type FeatureFlagService } from '../../feature-flag';
import { paramsParseOptions, preprocessParams } from '../../navigation/utils';
import type { WorkbenchView } from '../../workbench';
import type { WorkspaceService } from '../../workspace';
@@ -196,7 +192,6 @@ export class Editor extends Entity {
throw new Error('already bound');
}
this._setupBlocksuiteEditorFlags(editorContainer);
this.editorContainer$.next(editorContainer);
const unsubs: (() => void)[] = [];
@@ -325,24 +320,9 @@ export class Editor extends Entity {
};
}
private _setupBlocksuiteEditorFlags(editorContainer: AffineEditorContainer) {
const affineFeatureFlagService = this.featureFlagService;
const bsFeatureFlagService = editorContainer.doc.get(BSFeatureFlagService);
Object.entries(AFFINE_FLAGS).forEach(([key, flag]) => {
if (flag.category === 'blocksuite') {
const value =
affineFeatureFlagService.flags[key as keyof AFFINE_FLAGS].value;
if (value !== undefined) {
bsFeatureFlagService.setFlag(flag.bsFlag, value);
}
}
});
}
constructor(
private readonly docService: DocService,
private readonly workspaceService: WorkspaceService,
private readonly featureFlagService: FeatureFlagService
private readonly workspaceService: WorkspaceService
) {
super();
}

View File

@@ -1,7 +1,6 @@
import { type Framework } from '@toeverything/infra';
import { DocScope, DocService } from '../doc';
import { FeatureFlagService } from '../feature-flag';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { Editor } from './entities/editor';
import { EditorScope } from './scopes/editor';
@@ -19,7 +18,7 @@ export function configureEditorModule(framework: Framework) {
.scope(WorkspaceScope)
.scope(DocScope)
.service(EditorsService)
.entity(Editor, [DocService, WorkspaceService, FeatureFlagService])
.entity(Editor, [DocService, WorkspaceService])
.scope(EditorScope)
.service(EditorService, [EditorScope]);
}

View File

@@ -35,7 +35,7 @@ export class ImportClipperService extends Service {
collection: workspace.docCollection,
schema: getAFFiNEWorkspaceSchema(),
markdown: clipperInput.contentMarkdown,
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
});
const docsService = workspace.scope.get(DocsService);
if (docId) {
@@ -69,7 +69,7 @@ export class ImportClipperService extends Service {
collection: docCollection,
schema: getAFFiNEWorkspaceSchema(),
markdown: clipperInput.contentMarkdown,
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
});
}
);

View File

@@ -61,7 +61,7 @@ export class IntegrationWriter extends Entity {
schema: getAFFiNEWorkspaceSchema(),
markdown,
fileName: title,
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
});
if (!newDocId) throw new Error('Failed to create a new doc');
@@ -85,7 +85,7 @@ export class IntegrationWriter extends Entity {
doc,
blockId: noteBlockId,
markdown,
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
});
} else if (updateStrategy === 'append') {
const pageBlockId = doc.getBlocksByFlavour('affine:page')[0]?.id;
@@ -94,7 +94,7 @@ export class IntegrationWriter extends Entity {
doc,
blockId,
markdown: `---\n${markdown}`,
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
});
} else {
throw new Error('Invalid update strategy');

View File

@@ -14,8 +14,6 @@ export { NbstoreService } from './services/nbstore';
import { type Framework } from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api';
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
import {
IDBGlobalState,
LocalStorageGlobalCache,
@@ -49,12 +47,6 @@ export function configureLocalStorageStateStorageImpls(framework: Framework) {
framework.impl(CacheStorage, IDBGlobalState);
}
export function configureElectronStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
framework.impl(CacheStorage, IDBGlobalState);
}
export function configureCommonGlobalStorageImpls(framework: Framework) {
framework.impl(GlobalSessionState, SessionStorageGlobalSessionState);
}

View File

@@ -1,3 +1,4 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { Workspace as WorkspaceInterface } from '@blocksuite/affine/store';
import { Entity, LiveData, yjsGetPath } from '@toeverything/infra';
import type { Observable } from 'rxjs';
@@ -9,7 +10,10 @@ import type { WorkspaceScope } from '../scopes/workspace';
import { WorkspaceEngineService } from '../services/engine';
export class Workspace extends Entity {
constructor(public readonly scope: WorkspaceScope) {
constructor(
public readonly scope: WorkspaceScope,
public readonly featureFlagService: FeatureFlagService
) {
super();
}
@@ -30,6 +34,7 @@ export class Workspace extends Entity {
this._docCollection = new WorkspaceImpl({
id: this.openOptions.metadata.id,
rootDoc: this.rootYDoc,
featureFlagService: this.featureFlagService,
blobSource: {
get: async key => {
const record = await this.engine.blob.get(key);

View File

@@ -5,20 +5,21 @@ import {
type ExtensionType,
type GetStoreOptions,
StoreContainer,
type Workspace,
type YBlock,
} from '@blocksuite/affine/store';
import { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs';
import type { WorkspaceImpl } from './workspace';
type DocOptions = {
id: string;
collection: Workspace;
collection: WorkspaceImpl;
doc: Y.Doc;
};
export class DocImpl implements Doc {
private readonly _collection: Workspace;
private readonly _collection: WorkspaceImpl;
private readonly _storeContainer: StoreContainer;
@@ -136,7 +137,10 @@ export class DocImpl implements Doc {
extensions,
id,
}: GetStoreOptions = {}) {
const storeExtensions = getStoreManager().get('store');
const storeExtensions = getStoreManager()
.config.init()
.featureFlag(this.workspace.featureFlagService)
.value.get('store');
const exts = storeExtensions
.concat(extensions ?? [])
.concat(this.storeExtensions);

View File

@@ -19,6 +19,7 @@ import { Subject } from 'rxjs';
import type { Awareness } from 'y-protocols/awareness.js';
import type { Doc as YDoc } from 'yjs';
import type { FeatureFlagService } from '../../feature-flag';
import { DocImpl } from './doc';
import { WorkspaceMetaImpl } from './meta';
@@ -29,6 +30,7 @@ type WorkspaceOptions = {
onLoadDoc?: (doc: YDoc) => void;
onLoadAwareness?: (awareness: Awareness) => void;
onCreateDoc?: (docId?: string) => string;
featureFlagService?: FeatureFlagService;
};
export class WorkspaceImpl implements Workspace {
@@ -57,6 +59,7 @@ export class WorkspaceImpl implements Workspace {
readonly onLoadDoc?: (doc: YDoc) => void;
readonly onLoadAwareness?: (awareness: Awareness) => void;
readonly onCreateDoc?: (docId?: string) => string;
readonly featureFlagService?: FeatureFlagService;
constructor({
id,
@@ -65,8 +68,10 @@ export class WorkspaceImpl implements Workspace {
onLoadDoc,
onLoadAwareness,
onCreateDoc,
featureFlagService,
}: WorkspaceOptions) {
this.id = id || '';
this.featureFlagService = featureFlagService;
this.doc = rootDoc;
this.onLoadDoc = onLoadDoc;
this.onLoadDoc?.(this.doc);

View File

@@ -1,3 +1,5 @@
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
export type { WorkspaceProfileInfo } from './entities/profile';
export { Workspace } from './entities/workspace';
export { WorkspaceEngineBeforeStart, WorkspaceInitialized } from './events';
@@ -70,7 +72,7 @@ export function configureWorkspaceModule(framework: Framework) {
])
.scope(WorkspaceScope)
.service(WorkspaceService)
.entity(Workspace, [WorkspaceScope])
.entity(Workspace, [WorkspaceScope, FeatureFlagService])
.service(WorkspaceEngineService, [WorkspaceScope])
.entity(WorkspaceEngine, [WorkspaceService, NbstoreService])
.impl(WorkspaceLocalState, WorkspaceLocalStateImpl, [