snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 04:01:20 +05:00
commit 702cc698a7
9990 changed files with 1073618 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
import type { Container } from '@blocksuite/global/di';
/**
* # Understanding Extensions
*
* Extensions provide a way to extend the functionality of a system using dependency injection.
* They allow you to register services, implementations, and factories in the DI container,
* which can then be retrieved and used by different parts of the application.
*
* Extensions are particularly useful for:
* - Registering different implementations for different types
* - Creating pluggable architecture where components can be added or removed
* - Managing dependencies between different parts of the application
*
* # Usage Example: Fruit Processing System
*
* Let's consider a fruit processing system where different types of fruits need
* different processing methods. We'll show how to implement this using extensions.
*
* ## Step 1: Define the interfaces
*
* ```ts
* interface FruitProcessor {
* process(fruit: Fruit): void;
* }
*
* interface Fruit {
* type: string;
* // other properties
* }
* ```
*
* ## Step 2: Create a service identifier
*
* ```ts
* import { createIdentifier } from '@blocksuite/global/di';
*
* const FruitProcessorProvider = createIdentifier<FruitProcessor>('fruit-processor-provider');
* ```
*
* ## Step 3: Create implementations
*
* ```ts
* class AppleProcessor implements FruitProcessor {
* process(fruit: Fruit): void {
* console.log('Slicing apple');
* // Apple-specific processing
* }
* }
*
* class BananaProcessor implements FruitProcessor {
* process(fruit: Fruit): void {
* console.log('Peeling banana');
* // Banana-specific processing
* }
* }
* ```
*
* ## Step 4: Create an extension factory
*
* ```ts
* const FruitProcessorExtension = (
* fruitType: string,
* implementation: new () => FruitProcessor
* ): ExtensionType => {
* return {
* setup: di => {
* di.addImpl(FruitProcessorProvider(fruitType), implementation);
* }
* };
* };
* ```
*
* ## Step 5: Create concrete extensions
*
* ```ts
* export const AppleProcessorExtension = FruitProcessorExtension('apple', AppleProcessor);
* export const BananaProcessorExtension = FruitProcessorExtension('banana', BananaProcessor);
* ```
*
* ## Step 6: Use the extensions
*
* ```ts
* import { Container } from '@blocksuite/global/di';
*
* class FruitProcessingSystem {
* provider: ServiceProvider;
*
* constructor(extensions: ExtensionType[]) {
* const container = new Container();
*
* // Set up all extensions
* extensions.forEach(ext => ext.setup(container));
*
* // Create a provider from the container
* this.provider = container.provider();
* }
*
* processFruit(fruit: Fruit) {
* // Get the appropriate processor based on fruit type
* const processor = this.provider.get(FruitProcessorProvider(fruit.type));
*
* // Process the fruit
* processor.process(fruit);
* }
* }
*
* // Initialize the system with extensions
* const system = new FruitProcessingSystem([
* AppleProcessorExtension,
* BananaProcessorExtension
* ]);
*
* // Use the system
* system.processFruit({ type: 'apple' }); // Output: Slicing apple
* system.processFruit({ type: 'banana' }); // Output: Peeling banana
* ```
*
* Note: We deliberately used a non-block specific example here. In BlockSuite, the extension
* pattern can be applied to any entity that can be configured by third parties, not just blocks.
* This includes different tools in the whiteboard, different column types in database blocks,
* and many other extensible components. The pattern remains the same regardless of what you're extending.
*
* @category Extension
*/
export abstract class Extension {
static setup(_di: Container): void {
// do nothing
}
}
export interface ExtensionType {
setup(di: Container): void;
}

View File

@@ -0,0 +1,79 @@
import { signal } from '@preact/signals-core';
import { Subject } from 'rxjs';
import * as Y from 'yjs';
import type { Store } from '../../model';
import { StoreExtension } from '../store-extension';
export class HistoryExtension extends StoreExtension {
static override readonly key = 'history';
private readonly _history!: Y.UndoManager;
private readonly _canRedo = signal(false);
private readonly _canUndo = signal(false);
readonly onUpdated = new Subject<void>();
constructor(store: Store) {
super(store);
this._history = new Y.UndoManager([this.store.doc.yBlocks], {
trackedOrigins: new Set([this.store.doc.spaceDoc.clientID]),
});
}
private readonly _updateCanUndoRedoSignals = () => {
const canRedo = this._history.canRedo();
const canUndo = this._history.canUndo();
if (this._canRedo.peek() !== canRedo) {
this._canRedo.value = canRedo;
}
if (this._canUndo.peek() !== canUndo) {
this._canUndo.value = canUndo;
}
};
get canRedo() {
return this._canRedo.peek();
}
get canUndo() {
return this._canUndo.peek();
}
get canRedo$() {
return this._canRedo;
}
get canUndo$() {
return this._canUndo;
}
get undoManager() {
return this._history;
}
override loaded() {
this._updateCanUndoRedoSignals();
this._history.on('stack-cleared', this._historyObserver);
this._history.on('stack-item-added', this._historyObserver);
this._history.on('stack-item-popped', this._historyObserver);
this._history.on('stack-item-updated', this._historyObserver);
}
private readonly _historyObserver = () => {
this._updateCanUndoRedoSignals();
this.onUpdated.next();
};
override disposed() {
super.disposed();
this._history.off('stack-cleared', this._historyObserver);
this._history.off('stack-item-added', this._historyObserver);
this._history.off('stack-item-popped', this._historyObserver);
this._history.off('stack-item-updated', this._historyObserver);
this.onUpdated.complete();
}
}

View File

@@ -0,0 +1 @@
export * from './history-extension';

View File

@@ -0,0 +1,6 @@
export * from './extension';
export * from './history';
export * from './schema';
export * from './selection';
export * from './store-extension';
export * from './workspace';

View File

@@ -0,0 +1,20 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { BlockSchemaType } from '../model/block/zod';
import type { ExtensionType } from './extension';
export const BlockSchemaIdentifier =
createIdentifier<BlockSchemaType>('BlockSchema');
export function BlockSchemaExtension(
blockSchema: BlockSchemaType
): ExtensionType {
return {
setup: di => {
di.addImpl(
BlockSchemaIdentifier(blockSchema.model.flavour),
() => blockSchema
);
},
};
}

View File

@@ -0,0 +1,46 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { SelectionConstructor } from './types';
export type BaseSelectionOptions = {
blockId: string;
};
export abstract class BaseSelection {
static readonly group: string;
static readonly type: string;
static readonly recoverable: boolean = false;
readonly blockId: string;
get group(): string {
return (this.constructor as SelectionConstructor).group;
}
get type(): string {
return (this.constructor as SelectionConstructor).type as string;
}
constructor({ blockId }: BaseSelectionOptions) {
this.blockId = blockId;
}
static fromJSON(_: Record<string, unknown>): BaseSelection {
throw new BlockSuiteError(
ErrorCode.SelectionError,
'You must override this method'
);
}
abstract equals(other: BaseSelection): boolean;
is<T extends SelectionConstructor>(
type: T
): this is T extends SelectionConstructor<infer U> ? U : never {
return this.type === type.type;
}
abstract toJSON(): Record<string, unknown>;
}

View File

@@ -0,0 +1,17 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '../extension';
import type { SelectionConstructor } from './types';
export const SelectionIdentifier =
createIdentifier<SelectionConstructor>('Selection');
export function SelectionExtension(
selectionCtor: SelectionConstructor
): ExtensionType {
return {
setup: di => {
di.addImpl(SelectionIdentifier(selectionCtor.type), () => selectionCtor);
},
};
}

View File

@@ -0,0 +1,4 @@
export * from './base';
export * from './identifier';
export * from './selection-extension';
export * from './types';

View File

@@ -0,0 +1,189 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { computed, signal } from '@preact/signals-core';
import { Subject } from 'rxjs';
import { nanoid } from '../../utils/id-generator';
import type { StackItem } from '../../yjs';
import { StoreExtension } from '../store-extension';
import type { BaseSelection } from './base';
import { SelectionIdentifier } from './identifier';
import type { SelectionConstructor } from './types';
export class StoreSelectionExtension extends StoreExtension {
static override readonly key = 'selection';
private readonly _id = `${this.store.id}:${nanoid()}`;
private _selectionConstructors: Record<string, SelectionConstructor> = {};
private readonly _selections = signal<BaseSelection[]>([]);
private readonly _remoteSelections = signal<Map<number, BaseSelection[]>>(
new Map()
);
private readonly _itemAdded = (event: { stackItem: StackItem }) => {
event.stackItem.meta.set('selection-state', this._selections.value);
};
private readonly _itemPopped = (event: { stackItem: StackItem }) => {
const selection = event.stackItem.meta.get('selection-state');
if (selection) {
this.set(selection as BaseSelection[]);
}
};
private readonly _jsonToSelection = (json: Record<string, unknown>) => {
const ctor = this._selectionConstructors[json.type as string];
if (!ctor) {
throw new BlockSuiteError(
ErrorCode.SelectionError,
`Unknown selection type: ${json.type}`
);
}
return ctor.fromJSON(json);
};
slots = {
changed: new Subject<BaseSelection[]>(),
remoteChanged: new Subject<Map<number, BaseSelection[]>>(),
};
override loaded() {
this.store.provider.getAll(SelectionIdentifier).forEach(ctor => {
[ctor].flat().forEach(ctor => {
this._selectionConstructors[ctor.type] = ctor;
});
});
this.store.awarenessStore.awareness.on(
'change',
(change: { updated: number[]; added: number[]; removed: number[] }) => {
const all = change.updated.concat(change.added).concat(change.removed);
const localClientID = this.store.awarenessStore.awareness.clientID;
const exceptLocal = all.filter(id => id !== localClientID);
// Only consider remote selections from other clients
if (exceptLocal.length > 0) {
const map = new Map<number, BaseSelection[]>();
this.store.awarenessStore.getStates().forEach((state, id) => {
if (id === this.store.awarenessStore.awareness.clientID) return;
// selection id starts with the same block collection id from others clients would be considered as remote selections
const selection = Object.entries(state.selectionV2)
.filter(([key]) => key.startsWith(this.store.id))
.flatMap(([_, selection]) => selection);
const selections = selection
.map(json => {
try {
return this._jsonToSelection(json);
} catch (error) {
console.error(
'Parse remote selection failed:',
id,
json,
error
);
return null;
}
})
.filter((sel): sel is BaseSelection => !!sel);
map.set(id, selections);
});
this._remoteSelections.value = map;
this.slots.remoteChanged.next(map);
}
}
);
this.store.history.undoManager.on('stack-item-added', this._itemAdded);
this.store.history.undoManager.on('stack-item-popped', this._itemPopped);
}
override disposed() {
super.disposed();
this.store.history.undoManager.off('stack-item-added', this._itemAdded);
this.store.history.undoManager.off('stack-item-popped', this._itemPopped);
}
get value() {
return this._selections.value;
}
get remoteSelections() {
return this._remoteSelections.value;
}
clear(types?: string[]) {
if (types) {
const values = this.value.filter(
selection => !types.includes(selection.type)
);
this.set(values);
} else {
this.set([]);
}
}
create<T extends SelectionConstructor>(
Type: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Type(...args) as InstanceType<T>;
}
getGroup(group: string) {
return this.value.filter(s => s.group === group);
}
filter<T extends SelectionConstructor>(type: T) {
return this.filter$(type).value;
}
filter$<T extends SelectionConstructor>(type: T) {
return computed(() =>
this.value.filter((sel): sel is InstanceType<T> => sel.is(type))
);
}
find<T extends SelectionConstructor>(type: T) {
return this.find$(type).value;
}
find$<T extends SelectionConstructor>(type: T) {
return computed(() =>
this.value.find((sel): sel is InstanceType<T> => sel.is(type))
);
}
set(selections: BaseSelection[]) {
this.store.awarenessStore.setLocalSelection(
this._id,
selections.map(s => s.toJSON())
);
this._selections.value = selections;
this.slots.changed.next(selections);
}
setGroup(group: string, selections: BaseSelection[]) {
const current = this.value.filter(s => s.group !== group);
this.set([...current, ...selections]);
}
// This method is used to clear **current editor's remote selections**
// When the editor is not active, the remote selections should be cleared
// So other editors won't see the remote selections from this editor
clearRemote() {
this.store.awarenessStore.setLocalSelection(this._id, []);
}
update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]) {
const selections = fn(this.value);
this.set(selections);
}
fromJSON(json: Record<string, unknown>[]) {
const selections = json.map(json => {
return this._jsonToSelection(json);
});
return this.set(selections);
}
}

View File

@@ -0,0 +1,9 @@
import type { BaseSelection } from './base';
export interface SelectionConstructor<T extends BaseSelection = BaseSelection> {
type: string;
group: string;
new (...args: any[]): T;
fromJSON(json: Record<string, unknown>): T;
}

View File

@@ -0,0 +1,61 @@
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { Store } from '../model/store';
import { StoreIdentifier } from '../model/store/identifier';
import { Extension } from './extension';
export const StoreExtensionIdentifier =
createIdentifier<StoreExtension>('StoreExtension');
export const storeExtensionSymbol = Symbol('StoreExtension');
/**
* Store extensions are used to extend the store.
* They should be registered to the store. And they should be able to run in a none-dom environment.
*
* @category Extension
*/
export class StoreExtension extends Extension {
/**
* The key of the store extension.
* **You must override this property with a unique string.**
*/
static readonly key: string;
constructor(readonly store: Store) {
super();
}
/**
* Lifecycle hook when the yjs document is loaded.
*/
loaded() {}
/**
* Lifecycle hook when the yjs document is disposed.
*/
disposed() {}
static readonly [storeExtensionSymbol] = true;
static override setup(di: Container) {
if (!this.key) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'Key is not defined in the StoreExtension'
);
}
di.add(this, [StoreIdentifier]);
di.addImpl(StoreExtensionIdentifier(this.key), provider =>
provider.get(this)
);
}
}
export function isStoreExtensionConstructor(
extension: object
): extension is typeof StoreExtension {
return storeExtensionSymbol in extension;
}

View File

@@ -0,0 +1,38 @@
import { createIdentifier } from '@blocksuite/global/di';
import type * as Y from 'yjs';
import type { Store, StoreOptions, YBlock } from '../../model';
import type { AwarenessStore } from '../../yjs';
import type { Workspace } from './workspace.js';
import type { DocMeta } from './workspace-meta.js';
export type GetStoreOptions = Omit<StoreOptions, 'schema' | 'doc'>;
export type RemoveStoreOptions = Pick<
StoreOptions,
'query' | 'id' | 'readonly'
>;
export interface Doc {
readonly id: string;
get meta(): DocMeta | undefined;
remove(): void;
load(initFn?: () => void): void;
get ready(): boolean;
dispose(): void;
clear(): void;
getStore(options?: GetStoreOptions): Store;
removeStore(options: RemoveStoreOptions): void;
get loaded(): boolean;
get awarenessStore(): AwarenessStore;
get workspace(): Workspace;
get rootDoc(): Y.Doc;
get spaceDoc(): Y.Doc;
get yBlocks(): Y.Map<YBlock>;
}
export const DocIdentifier = createIdentifier<Doc>('store-doc');

View File

@@ -0,0 +1,3 @@
export * from './doc';
export * from './workspace';
export * from './workspace-meta';

View File

@@ -0,0 +1,40 @@
import type { Subject } from 'rxjs';
export type Tag = {
id: string;
value: string;
color: string;
};
export type DocsPropertiesMeta = {
tags?: {
options: Tag[];
};
};
export interface DocMeta {
id: string;
title: string;
tags: string[];
createDate: number;
updatedDate?: number;
favorite?: boolean;
trash?: boolean;
}
export interface WorkspaceMeta {
get docMetas(): DocMeta[];
addDocMeta(props: DocMeta, index?: number): void;
getDocMeta(id: string): DocMeta | undefined;
setDocMeta(id: string, props: Partial<DocMeta>): void;
removeDocMeta(id: string): void;
get properties(): DocsPropertiesMeta;
setProperties(meta: DocsPropertiesMeta): void;
get docs(): unknown[] | undefined;
initialize(): void;
docMetaAdded: Subject<string>;
docMetaRemoved: Subject<string>;
docMetaUpdated: Subject<void>;
}

View File

@@ -0,0 +1,30 @@
import type { BlobEngine } from '@blocksuite/sync';
import type { Subject } from 'rxjs';
import type { Awareness } from 'y-protocols/awareness.js';
import type * as Y from 'yjs';
import type { IdGenerator } from '../../utils/id-generator';
import type { Doc } from './doc.js';
import type { WorkspaceMeta } from './workspace-meta.js';
export interface Workspace {
readonly id: string;
readonly meta: WorkspaceMeta;
readonly idGenerator: IdGenerator;
readonly blobSync: BlobEngine;
readonly onLoadDoc?: (doc: Y.Doc) => void;
readonly onLoadAwareness?: (awareness: Awareness) => void;
get doc(): Y.Doc;
get docs(): Map<string, Doc>;
slots: {
docListUpdated: Subject<void>;
};
createDoc(docId?: string): Doc;
getDoc(docId: string): Doc | null;
removeDoc(docId: string): void;
dispose(): void;
}