chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,4 @@
import type { ServiceVariant } from './types.js';
export const DEFAULT_SERVICE_VARIANT: ServiceVariant = 'default';
export const ROOT_SCOPE = [];
@@ -0,0 +1,459 @@
import { DEFAULT_SERVICE_VARIANT, ROOT_SCOPE } from './consts.js';
import { DuplicateServiceDefinitionError } from './error.js';
import { parseIdentifier } from './identifier.js';
import type { ServiceProvider } from './provider.js';
import { BasicServiceProvider } from './provider.js';
import { stringifyScope } from './scope.js';
import type {
GeneralServiceIdentifier,
ServiceFactory,
ServiceIdentifier,
ServiceIdentifierType,
ServiceIdentifierValue,
ServiceScope,
ServiceVariant,
Type,
TypesToDeps,
} from './types.js';
/**
* A container of services.
*
* Container basically is a tuple of `[scope, identifier, variant, factory]` with some helper methods.
* It just stores the definitions of services. It never holds any instances of services.
*
* # Usage
*
* ```ts
* const services = new Container();
* class ServiceA {
* // ...
* }
* // add a service
* services.add(ServiceA);
*
* class ServiceB {
* constructor(serviceA: ServiceA) {}
* }
* // add a service with dependency
* services.add(ServiceB, [ServiceA]);
* ^ dependency class/identifier, match ServiceB's constructor
*
* const FeatureA = createIdentifier<FeatureA>('Config');
*
* // add a implementation for a service identifier
* services.addImpl(FeatureA, ServiceA);
*
* // override a service
* services.override(ServiceA, NewServiceA);
*
* // create a service provider
* const provider = services.provider();
* ```
*
* # The data structure
*
* The data structure of Container is a three-layer nested Map, used to represent the tuple of
* `[scope, identifier, variant, factory]`.
* Such a data structure ensures that a service factory can be uniquely determined by `[scope, identifier, variant]`.
*
* When a service added:
*
* ```ts
* services.add(ServiceClass)
* ```
*
* The data structure will be:
*
* ```ts
* Map {
* '': Map { // scope
* 'ServiceClass': Map { // identifier
* 'default': // variant
* () => new ServiceClass() // factory
* }
* }
* ```
*
* # Dependency relationship
*
* The dependency relationships of services are not actually stored in the Container,
* but are transformed into a factory function when the service is added.
*
* For example:
*
* ```ts
* services.add(ServiceB, [ServiceA]);
*
* // is equivalent to
* services.addFactory(ServiceB, (provider) => new ServiceB(provider.get(ServiceA)));
* ```
*
* For multiple implementations of the same service identifier, can be defined as:
*
* ```ts
* services.add(ServiceB, [[FeatureA]]);
*
* // is equivalent to
* services.addFactory(ServiceB, (provider) => new ServiceB(provider.getAll(FeatureA)));
* ```
*/
export class Container {
private readonly services = new Map<
string,
Map<string, Map<ServiceVariant, ServiceFactory>>
>();
/**
* @see {@link ContainerEditor.add}
*/
get add() {
return new ContainerEditor(this).add;
}
/**
* @see {@link ContainerEditor.addImpl}
*/
get addImpl() {
return new ContainerEditor(this).addImpl;
}
/**
* Create an empty service container.
*
* same as `new Container()`
*/
static get EMPTY() {
return new Container();
}
/**
* @see {@link ContainerEditor.scope}
*/
get override() {
return new ContainerEditor(this).override;
}
/**
* @see {@link ContainerEditor.scope}
*/
get scope() {
return new ContainerEditor(this).scope;
}
/**
* The number of services in the container.
*/
get size() {
let size = 0;
for (const [, identifiers] of this.services) {
for (const [, variants] of identifiers) {
size += variants.size;
}
}
return size;
}
/**
* @internal Use {@link addImpl} instead.
*/
addFactory<T>(
identifier: GeneralServiceIdentifier<T>,
factory: ServiceFactory<T>,
{ scope, override }: { scope?: ServiceScope; override?: boolean } = {}
) {
// convert scope to string
const normalizedScope = stringifyScope(scope ?? ROOT_SCOPE);
const normalizedIdentifier = parseIdentifier(identifier);
const normalizedVariant =
normalizedIdentifier.variant ?? DEFAULT_SERVICE_VARIANT;
const services =
this.services.get(normalizedScope) ??
new Map<string, Map<ServiceVariant, ServiceFactory>>();
const variants =
services.get(normalizedIdentifier.identifierName) ??
new Map<ServiceVariant, ServiceFactory>();
// throw if service already exists, unless it is an override
if (variants.has(normalizedVariant) && !override) {
throw new DuplicateServiceDefinitionError(normalizedIdentifier);
}
variants.set(normalizedVariant, factory);
services.set(normalizedIdentifier.identifierName, variants);
this.services.set(normalizedScope, services);
}
/**
* @internal Use {@link addImpl} instead.
*/
addValue<T>(
identifier: GeneralServiceIdentifier<T>,
value: T,
{ scope, override }: { scope?: ServiceScope; override?: boolean } = {}
) {
this.addFactory(
parseIdentifier(identifier) as ServiceIdentifier<T>,
() => value,
{
scope,
override,
}
);
}
/**
* Clone the entire service container.
*
* This method is quite cheap as it only clones the references.
*
* @returns A new service container with the same services.
*/
clone(): Container {
const di = new Container();
for (const [scope, identifiers] of this.services) {
const s = new Map();
for (const [identifier, variants] of identifiers) {
s.set(identifier, new Map(variants));
}
di.services.set(scope, s);
}
return di;
}
/**
* @internal
*/
getFactory(
identifier: ServiceIdentifierValue,
scope: ServiceScope = ROOT_SCOPE
): ServiceFactory | undefined {
return this.services
.get(stringifyScope(scope))
?.get(identifier.identifierName)
?.get(identifier.variant ?? DEFAULT_SERVICE_VARIANT);
}
/**
* @internal
*/
getFactoryAll(
identifier: ServiceIdentifierValue,
scope: ServiceScope = ROOT_SCOPE
): Map<ServiceVariant, ServiceFactory> {
return new Map(
this.services.get(stringifyScope(scope))?.get(identifier.identifierName)
);
}
/**
* Create a service provider from the container.
*
* @example
* ```ts
* provider() // create a service provider for root scope
* provider(ScopeA, parentProvider) // create a service provider for scope A
* ```
*
* @param scope The scope of the service provider, default to the root scope.
* @param parent The parent service provider, it is required if the scope is not the root scope.
*/
provider(
scope: ServiceScope = ROOT_SCOPE,
parent: ServiceProvider | null = null
): ServiceProvider {
return new BasicServiceProvider(this, scope, parent);
}
}
/**
* A helper class to edit a service container.
*/
class ContainerEditor {
private currentScope: ServiceScope = ROOT_SCOPE;
/**
* Add a service to the container.
*
* @see {@link Container}
*
* @example
* ```ts
* add(ServiceClass, [dependencies, ...])
* ```
*/
add = <
T extends new (...args: any) => any,
const Deps extends TypesToDeps<ConstructorParameters<T>> = TypesToDeps<
ConstructorParameters<T>
>,
>(
cls: T,
...[deps]: Deps extends [] ? [] : [Deps]
): this => {
this.container.addFactory<any>(
cls as any,
dependenciesToFactory(cls, deps as any),
{ scope: this.currentScope }
);
return this;
};
/**
* Add an implementation for identifier to the container.
*
* @see {@link Container}
*
* @example
* ```ts
* addImpl(ServiceIdentifier, ServiceClass, [dependencies, ...])
* or
* addImpl(ServiceIdentifier, Instance)
* or
* addImpl(ServiceIdentifier, Factory)
* ```
*/
addImpl = <
Arg1 extends ServiceIdentifier<any>,
Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait,
Trait = ServiceIdentifierType<Arg1>,
Deps extends Arg2 extends Type<Trait>
? TypesToDeps<ConstructorParameters<Arg2>>
: [] = Arg2 extends Type<Trait>
? TypesToDeps<ConstructorParameters<Arg2>>
: [],
Arg3 extends Deps = Deps,
>(
identifier: Arg1,
arg2: Arg2,
...[arg3]: Arg3 extends [] ? [] : [Arg3]
): this => {
if (arg2 instanceof Function) {
this.container.addFactory<any>(
identifier,
dependenciesToFactory(arg2, arg3 as any[]),
{ scope: this.currentScope }
);
} else {
this.container.addValue(identifier, arg2 as any, {
scope: this.currentScope,
});
}
return this;
};
/**
* same as {@link addImpl} but this method will override the service if it exists.
*
* @see {@link Container}
*
* @example
* ```ts
* override(OriginServiceClass, NewServiceClass, [dependencies, ...])
* or
* override(ServiceIdentifier, ServiceClass, [dependencies, ...])
* or
* override(ServiceIdentifier, Instance)
* or
* override(ServiceIdentifier, Factory)
* ```
*/
override = <
Arg1 extends ServiceIdentifier<any>,
Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait,
Trait = ServiceIdentifierType<Arg1>,
Deps extends Arg2 extends Type<Trait>
? TypesToDeps<ConstructorParameters<Arg2>>
: [] = Arg2 extends Type<Trait>
? TypesToDeps<ConstructorParameters<Arg2>>
: [],
Arg3 extends Deps = Deps,
>(
identifier: Arg1,
arg2: Arg2,
...[arg3]: Arg3 extends [] ? [] : [Arg3]
): this => {
if (arg2 instanceof Function) {
this.container.addFactory<any>(
identifier,
dependenciesToFactory(arg2, arg3 as any[]),
{ scope: this.currentScope, override: true }
);
} else {
this.container.addValue(identifier, arg2 as any, {
scope: this.currentScope,
override: true,
});
}
return this;
};
/**
* Set the scope for the service registered subsequently
*
* @example
*
* ```ts
* const ScopeA = createScope('a');
*
* services.scope(ScopeA).add(XXXService, ...);
* ```
*/
scope = (scope: ServiceScope): ContainerEditor => {
this.currentScope = scope;
return this;
};
constructor(private readonly container: Container) {}
}
/**
* Convert dependencies definition to a factory function.
*/
function dependenciesToFactory(
cls: any,
deps: any[] = []
): ServiceFactory<any> {
return (provider: ServiceProvider) => {
const args = [];
for (const dep of deps) {
let isAll;
let identifier;
if (Array.isArray(dep)) {
if (dep.length !== 1) {
throw new Error('Invalid dependency');
}
isAll = true;
identifier = dep[0];
} else {
isAll = false;
identifier = dep;
}
if (isAll) {
args.push(Array.from(provider.getAll(identifier).values()));
} else {
args.push(provider.get(identifier));
}
}
if (isConstructor(cls)) {
return new cls(...args, provider);
} else {
return cls(...args, provider);
}
};
}
// a hack to check if a function is a constructor
// https://github.com/zloirock/core-js/blob/232c8462c26c75864b4397b7f643a4f57c6981d5/packages/core-js/internals/is-constructor.js#L15
function isConstructor(cls: unknown) {
try {
Reflect.construct(function () {}, [], cls as never);
return true;
} catch {
return false;
}
}
@@ -0,0 +1,59 @@
import { DEFAULT_SERVICE_VARIANT } from './consts.js';
import type { ServiceIdentifierValue } from './types.js';
export class RecursionLimitError extends Error {
constructor() {
super('Dynamic resolve recursion limit reached');
}
}
export class CircularDependencyError extends Error {
constructor(readonly dependencyStack: ServiceIdentifierValue[]) {
super(
`A circular dependency was detected.\n` +
stringifyDependencyStack(dependencyStack)
);
}
}
export class ServiceNotFoundError extends Error {
constructor(readonly identifier: ServiceIdentifierValue) {
super(`Service ${stringifyIdentifier(identifier)} not found in container`);
}
}
export class MissingDependencyError extends Error {
constructor(
readonly from: ServiceIdentifierValue,
readonly target: ServiceIdentifierValue,
readonly dependencyStack: ServiceIdentifierValue[]
) {
super(
`Missing dependency ${stringifyIdentifier(
target
)} in creating service ${stringifyIdentifier(
from
)}.\n${stringifyDependencyStack(dependencyStack)}`
);
}
}
export class DuplicateServiceDefinitionError extends Error {
constructor(readonly identifier: ServiceIdentifierValue) {
super(`Service ${stringifyIdentifier(identifier)} already exists`);
}
}
function stringifyIdentifier(identifier: ServiceIdentifierValue) {
return `[${identifier.identifierName}]${
identifier.variant !== DEFAULT_SERVICE_VARIANT
? `(${identifier.variant})`
: ''
}`;
}
function stringifyDependencyStack(dependencyStack: ServiceIdentifierValue[]) {
return dependencyStack
.map(identifier => `${stringifyIdentifier(identifier)}`)
.join(' -> ');
}
@@ -0,0 +1,113 @@
import { DEFAULT_SERVICE_VARIANT } from './consts.js';
import { stableHash } from './stable-hash.js';
import type {
ServiceIdentifier,
ServiceIdentifierValue,
ServiceVariant,
Type,
} from './types.js';
/**
* create a ServiceIdentifier.
*
* ServiceIdentifier is used to identify a certain type of service. With the identifier, you can reference one or more services
* without knowing the specific implementation, thereby achieving
* [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control).
*
* @example
* ```ts
* // define a interface
* interface Storage {
* get(key: string): string | null;
* set(key: string, value: string): void;
* }
*
* // create a identifier
* // NOTICE: Highly recommend to use the interface name as the identifier name,
* // so that it is easy to understand. and it is legal to do so in TypeScript.
* const Storage = createIdentifier<Storage>('Storage');
*
* // create a implementation
* class LocalStorage implements Storage {
* get(key: string): string | null {
* return localStorage.getItem(key);
* }
* set(key: string, value: string): void {
* localStorage.setItem(key, value);
* }
* }
*
* // register the implementation to the identifier
* services.addImpl(Storage, LocalStorage);
*
* // get the implementation from the identifier
* const storage = services.provider().get(Storage);
* storage.set('foo', 'bar');
* ```
*
* With identifier:
*
* * You can easily replace the implementation of a `Storage` without changing the code that uses it.
* * You can easily mock a `Storage` for testing.
*
* # Variant
*
* Sometimes, you may want to register multiple implementations for the same interface.
* For example, you may want have both `LocalStorage` and `SessionStorage` for `Storage`,
* and use them in same time.
*
* In this case, you can use `variant` to distinguish them.
*
* ```ts
* const Storage = createIdentifier<Storage>('Storage');
* const LocalStorage = Storage('local');
* const SessionStorage = Storage('session');
*
* services.addImpl(LocalStorage, LocalStorageImpl);
* services.addImpl(SessionStorage, SessionStorageImpl);
*
* // get the implementation from the identifier
* const localStorage = services.provider().get(LocalStorage);
* const sessionStorage = services.provider().get(SessionStorage);
* const storage = services.provider().getAll(Storage); // { local: LocalStorageImpl, session: SessionStorageImpl }
* ```
*
* @param name unique name of the identifier.
* @param variant The default variant name of the identifier, can be overridden by `identifier("variant")`.
*/
export function createIdentifier<T>(
name: string,
variant: ServiceVariant = DEFAULT_SERVICE_VARIANT
): ServiceIdentifier<T> & ((variant: ServiceVariant) => ServiceIdentifier<T>) {
return Object.assign(
(variant: ServiceVariant) => {
return createIdentifier<T>(name, variant);
},
{
identifierName: name,
variant,
}
) as never;
}
/**
* Convert the constructor into a ServiceIdentifier.
* As we always deal with ServiceIdentifier in the DI container.
*
* @internal
*/
export function createIdentifierFromConstructor<T>(
target: Type<T>
): ServiceIdentifier<T> {
return createIdentifier<T>(`${target.name}${stableHash(target)}`);
}
export function parseIdentifier(input: any): ServiceIdentifierValue {
if (input.identifierName) {
return input as ServiceIdentifierValue;
} else if (typeof input === 'function' && input.name) {
return createIdentifierFromConstructor(input);
} else {
throw new Error('Input is not a service identifier.');
}
}
@@ -0,0 +1,7 @@
export * from './consts.js';
export * from './container.js';
export * from './error.js';
export * from './identifier.js';
export * from './provider.js';
export * from './scope.js';
export * from './types.js';
@@ -0,0 +1,219 @@
import type { Container } from './container.js';
import {
CircularDependencyError,
MissingDependencyError,
RecursionLimitError,
ServiceNotFoundError,
} from './error.js';
import { parseIdentifier } from './identifier.js';
import type {
GeneralServiceIdentifier,
ServiceIdentifierValue,
ServiceVariant,
} from './types.js';
export interface ResolveOptions {
sameScope?: boolean;
optional?: boolean;
}
export abstract class ServiceProvider {
abstract container: Container;
get<T>(identifier: GeneralServiceIdentifier<T>, options?: ResolveOptions): T {
return this.getRaw(parseIdentifier(identifier), {
...options,
optional: false,
});
}
getAll<T>(
identifier: GeneralServiceIdentifier<T>,
options?: ResolveOptions
): Map<ServiceVariant, T> {
return this.getAllRaw(parseIdentifier(identifier), {
...options,
});
}
abstract getAllRaw(
identifier: ServiceIdentifierValue,
options?: ResolveOptions
): Map<ServiceVariant, any>;
getOptional<T>(
identifier: GeneralServiceIdentifier<T>,
options?: ResolveOptions
): T | null {
return this.getRaw(parseIdentifier(identifier), {
...options,
optional: true,
});
}
abstract getRaw(
identifier: ServiceIdentifierValue,
options?: ResolveOptions
): any;
}
export class ServiceCachePool {
cache = new Map<string, Map<ServiceVariant, any>>();
getOrInsert(identifier: ServiceIdentifierValue, insert: () => any) {
const cache = this.cache.get(identifier.identifierName) ?? new Map();
if (!cache.has(identifier.variant)) {
cache.set(identifier.variant, insert());
}
const cached = cache.get(identifier.variant);
this.cache.set(identifier.identifierName, cache);
return cached;
}
}
export class ServiceResolver extends ServiceProvider {
container = this.provider.container;
constructor(
readonly provider: BasicServiceProvider,
readonly depth = 0,
readonly stack: ServiceIdentifierValue[] = []
) {
super();
}
getAllRaw(
identifier: ServiceIdentifierValue,
{ sameScope = false }: ResolveOptions = {}
): Map<ServiceVariant, any> {
const vars = this.provider.container.getFactoryAll(
identifier,
this.provider.scope
);
if (vars === undefined) {
if (this.provider.parent && !sameScope) {
return this.provider.parent.getAllRaw(identifier);
}
return new Map();
}
const result = new Map<ServiceVariant, any>();
for (const [variant, factory] of vars) {
const service = this.provider.cache.getOrInsert(
{ identifierName: identifier.identifierName, variant },
() => {
const nextResolver = this.track(identifier);
try {
return factory(nextResolver);
} catch (err) {
if (err instanceof ServiceNotFoundError) {
throw new MissingDependencyError(
identifier,
err.identifier,
this.stack
);
}
throw err;
}
}
);
result.set(variant, service);
}
return result;
}
getRaw(
identifier: ServiceIdentifierValue,
{ sameScope = false, optional = false }: ResolveOptions = {}
) {
const factory = this.provider.container.getFactory(
identifier,
this.provider.scope
);
if (!factory) {
if (this.provider.parent && !sameScope) {
return this.provider.parent.getRaw(identifier, {
sameScope,
optional,
});
}
if (optional) {
return undefined;
}
throw new ServiceNotFoundError(identifier);
}
return this.provider.cache.getOrInsert(identifier, () => {
const nextResolver = this.track(identifier);
try {
return factory(nextResolver);
} catch (err) {
if (err instanceof ServiceNotFoundError) {
throw new MissingDependencyError(
identifier,
err.identifier,
this.stack
);
}
throw err;
}
});
}
track(identifier: ServiceIdentifierValue): ServiceResolver {
const depth = this.depth + 1;
if (depth >= 100) {
throw new RecursionLimitError();
}
const circular = this.stack.find(
i =>
i.identifierName === identifier.identifierName &&
i.variant === identifier.variant
);
if (circular) {
throw new CircularDependencyError([...this.stack, identifier]);
}
return new ServiceResolver(this.provider, depth, [
...this.stack,
identifier,
]);
}
}
export class BasicServiceProvider extends ServiceProvider {
readonly cache = new ServiceCachePool();
readonly container: Container;
constructor(
container: Container,
readonly scope: string[],
readonly parent: ServiceProvider | null
) {
super();
this.container = container.clone();
this.container.addValue(ServiceProvider, this, {
scope: scope,
override: true,
});
}
getAllRaw(
identifier: ServiceIdentifierValue,
options?: ResolveOptions
): Map<ServiceVariant, any> {
const resolver = new ServiceResolver(this);
return resolver.getAllRaw(identifier, options);
}
getRaw(identifier: ServiceIdentifierValue, options?: ResolveOptions) {
const resolver = new ServiceResolver(this);
return resolver.getRaw(identifier, options);
}
}
@@ -0,0 +1,13 @@
import { ROOT_SCOPE } from './consts.js';
import type { ServiceScope } from './types.js';
export function createScope(
name: string,
base: ServiceScope = ROOT_SCOPE
): ServiceScope {
return [...base, name];
}
export function stringifyScope(scope: ServiceScope): string {
return scope.join('/');
}
@@ -0,0 +1,59 @@
// copied from https://github.com/shuding/stable-hash
// Use WeakMap to store the object-key mapping so the objects can still be
// garbage collected. WeakMap uses a hashtable under the hood, so the lookup
// complexity is almost O(1).
const table = new WeakMap<object, string>();
// A counter of the key.
let counter = 0;
// A stable hash implementation that supports:
// - Fast and ensures unique hash properties
// - Handles unserializable values
// - Handles object key ordering
// - Generates short results
//
// This is not a serialization function, and the result is not guaranteed to be
// parsable.
export function stableHash(arg: any): string {
const type = typeof arg;
const constructor = arg && arg.constructor;
const isDate = constructor === Date;
if (Object(arg) === arg && !isDate && constructor !== RegExp) {
// Object/function, not null/date/regexp. Use WeakMap to store the id first.
// If it's already hashed, directly return the result.
let result = table.get(arg);
if (result) return result;
// Store the hash first for circular reference detection before entering the
// recursive `stableHash` calls.
// For other objects like set and map, we use this id directly as the hash.
result = ++counter + '~';
table.set(arg, result);
let index: any;
if (constructor === Array) {
// Array.
result = '@';
for (index = 0; index < arg.length; index++) {
result += stableHash(arg[index]) + ',';
}
table.set(arg, result);
} else if (constructor === Object) {
// Object, sort keys.
result = '#';
const keys = Object.keys(arg).sort();
while ((index = keys.pop() as string) !== undefined) {
if (arg[index] !== undefined) {
result += index + ':' + stableHash(arg[index]) + ',';
}
}
table.set(arg, result);
}
return result;
}
if (isDate) return arg.toJSON();
if (type === 'symbol') return arg.toString();
return type === 'string' ? JSON.stringify(arg) : '' + arg;
}
@@ -0,0 +1,37 @@
import type { ServiceProvider } from './provider.js';
export type Type<T = any> = abstract new (...args: any) => T;
export type ServiceFactory<T = any> = (provider: ServiceProvider) => T;
export type ServiceVariant = string;
/**
*
*/
export type ServiceScope = string[];
export type ServiceIdentifierValue = {
identifierName: string;
variant: ServiceVariant;
};
export type GeneralServiceIdentifier<T = any> = ServiceIdentifier<T> | Type<T>;
export type ServiceIdentifier<T> = {
identifierName: string;
variant: ServiceVariant;
__TYPE__: T;
};
export type ServiceIdentifierType<T> =
T extends ServiceIdentifier<infer R>
? R
: T extends Type<infer R>
? R
: never;
export type TypesToDeps<T extends any[]> = {
[index in keyof T]:
| GeneralServiceIdentifier<T[index]>
| (T[index] extends (infer I)[] ? [GeneralServiceIdentifier<I>] : never);
};