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
@@ -252,7 +252,8 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
} else {
const container = new Container();
getStoreManager()
.get('store')
.config.init()
.value.get('store')
.forEach(ext => {
ext.setup(container);
});
@@ -7,7 +7,7 @@ import { markdownToMindmap } from '../mindmap-preview.js';
const container = new Container();
getStoreManager()
.get('store')
.value.get('store')
.forEach(ext => {
ext.setup(container);
});
@@ -99,7 +99,7 @@ export const usePageHelper = (docCollection: Workspace) => {
showImportModal({
collection: docCollection,
schema: getAFFiNEWorkspaceSchema(),
extensions: getStoreManager().get('store'),
extensions: getStoreManager().config.init().value.get('store'),
onSuccess,
onFail: message => {
reject(new Error(message));
@@ -0,0 +1,29 @@
import {
AFFINE_FLAGS,
type FeatureFlagService,
} from '@affine/core/modules/feature-flag';
import { FeatureFlagService as BSFeatureFlagService } from '@blocksuite/affine/shared/services';
import { type ExtensionType, StoreExtension } from '@blocksuite/affine/store';
export function getFeatureFlagSyncer(
featureFlagService: FeatureFlagService
): ExtensionType {
class FeatureFlagSyncer extends StoreExtension {
static override key = 'feature-flag-syncer';
override loaded() {
const bsFeatureFlagService = this.store.get(BSFeatureFlagService);
Object.entries(AFFINE_FLAGS).forEach(([key, flag]) => {
if (flag.category === 'blocksuite') {
const value =
featureFlagService.flags[key as keyof AFFINE_FLAGS].value;
if (value !== undefined) {
bsFeatureFlagService.setFlag(flag.bsFlag, value);
}
}
});
}
}
return FeatureFlagSyncer;
}
@@ -0,0 +1,29 @@
import { getFeatureFlagSyncer } from '@affine/core/blocksuite/extensions/feature-flag/feature-flag-syncer';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import {
type StoreExtensionContext,
StoreExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { z } from 'zod';
const optionsSchema = z.object({
featureFlagService: z.instanceof(FeatureFlagService).optional(),
});
export class FeatureFlagStoreExtension extends StoreExtensionProvider {
override name = 'feature-flag-store-extension';
override schema = optionsSchema;
override setup(
context: StoreExtensionContext,
options?: z.infer<typeof optionsSchema>
) {
super.setup(context, options);
const featureFlagService = options?.featureFlagService;
if (!featureFlagService) {
return;
}
context.register(getFeatureFlagSyncer(featureFlagService));
}
}
@@ -1,3 +1,5 @@
import { FeatureFlagStoreExtension } from '@affine/core/blocksuite/extensions/feature-flag';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import {
type StoreExtensionContext,
StoreExtensionManager,
@@ -18,11 +20,57 @@ class MigratingAffineStoreExtension extends StoreExtensionProvider {
}
}
const manager = new StoreExtensionManager([
...getInternalStoreExtensions(),
MigratingAffineStoreExtension,
]);
interface Configure {
init: () => Configure;
featureFlag: (featureFlagService?: FeatureFlagService) => Configure;
value: StoreExtensionManager;
}
class StoreProvider {
static instance: StoreProvider | null = null;
static getInstance() {
if (!StoreProvider.instance) {
StoreProvider.instance = new StoreProvider();
}
return StoreProvider.instance;
}
private readonly _manager: StoreExtensionManager;
constructor() {
this._manager = new StoreExtensionManager([
...getInternalStoreExtensions(),
MigratingAffineStoreExtension,
FeatureFlagStoreExtension,
]);
}
get config(): Configure {
return {
init: this._initDefaultConfig,
featureFlag: this._configureFeatureFlag,
value: this._manager,
};
}
get value(): StoreExtensionManager {
return this._manager;
}
private readonly _initDefaultConfig = () => {
this.config.featureFlag();
return this.config;
};
private readonly _configureFeatureFlag = (
featureFlagService?: FeatureFlagService
) => {
this._manager.configure(FeatureFlagStoreExtension, { featureFlagService });
return this.config;
};
}
export function getStoreManager() {
return manager;
return StoreProvider.getInstance();
}