mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
feat(infra): framework
This commit is contained in:
539
packages/common/infra/src/framework/__tests__/framework.spec.ts
Normal file
539
packages/common/infra/src/framework/__tests__/framework.spec.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
CircularDependencyError,
|
||||
ComponentNotFoundError,
|
||||
createEvent,
|
||||
createIdentifier,
|
||||
DuplicateDefinitionError,
|
||||
Entity,
|
||||
Framework,
|
||||
MissingDependencyError,
|
||||
RecursionLimitError,
|
||||
Scope,
|
||||
Service,
|
||||
} from '..';
|
||||
import { OnEvent } from '../core/event';
|
||||
|
||||
describe('framework', () => {
|
||||
test('basic', () => {
|
||||
const framework = new Framework();
|
||||
class TestService extends Service {
|
||||
a = 'b';
|
||||
}
|
||||
|
||||
framework.service(TestService);
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(provider.get(TestService).a).toBe('b');
|
||||
});
|
||||
|
||||
test('entity', () => {
|
||||
const framework = new Framework();
|
||||
class TestService extends Service {
|
||||
a = 'b';
|
||||
}
|
||||
|
||||
class TestEntity extends Entity<{ name: string }> {
|
||||
constructor(readonly test: TestService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(TestService).entity(TestEntity, [TestService]);
|
||||
|
||||
const provider = framework.provider();
|
||||
const entity = provider.createEntity(TestEntity, {
|
||||
name: 'test',
|
||||
});
|
||||
expect(entity.test.a).toBe('b');
|
||||
expect(entity.props.name).toBe('test');
|
||||
});
|
||||
|
||||
test('componentCount', () => {
|
||||
const framework = new Framework();
|
||||
class TestService extends Service {
|
||||
a = 'b';
|
||||
}
|
||||
|
||||
framework.service(TestService);
|
||||
|
||||
expect(framework.componentCount).toEqual(1);
|
||||
});
|
||||
|
||||
test('dependency', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
class A extends Service {
|
||||
value = 'hello world';
|
||||
}
|
||||
|
||||
class B extends Service {
|
||||
constructor(public a: A) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class C extends Service {
|
||||
constructor(public b: B) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(A).service(B, [A]).service(C, [B]);
|
||||
|
||||
const provider = framework.provider();
|
||||
|
||||
expect(provider.get(C).b.a.value).toEqual('hello world');
|
||||
});
|
||||
|
||||
test('identifier', () => {
|
||||
interface Animal extends Service {
|
||||
name: string;
|
||||
}
|
||||
const Animal = createIdentifier<Animal>('Animal');
|
||||
|
||||
class Cat extends Service {
|
||||
name = 'cat';
|
||||
}
|
||||
|
||||
class Zoo extends Service {
|
||||
constructor(public animal: Animal) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
const serviceCollection = new Framework();
|
||||
serviceCollection.impl(Animal, Cat).service(Zoo, [Animal]);
|
||||
|
||||
const provider = serviceCollection.provider();
|
||||
expect(provider.get(Zoo).animal.name).toEqual('cat');
|
||||
});
|
||||
|
||||
test('variant', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
interface USB extends Service {
|
||||
speed: number;
|
||||
}
|
||||
|
||||
const USB = createIdentifier<USB>('USB');
|
||||
|
||||
class TypeA extends Service implements USB {
|
||||
speed = 100;
|
||||
}
|
||||
class TypeC extends Service implements USB {
|
||||
speed = 300;
|
||||
}
|
||||
|
||||
class PC extends Service {
|
||||
constructor(
|
||||
public typeA: USB,
|
||||
public ports: USB[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework
|
||||
.impl(USB('A'), TypeA)
|
||||
.impl(USB('C'), TypeC)
|
||||
.service(PC, [USB('A'), [USB]]);
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(provider.get(USB('A')).speed).toEqual(100);
|
||||
expect(provider.get(USB('C')).speed).toEqual(300);
|
||||
expect(provider.get(PC).typeA.speed).toEqual(100);
|
||||
expect(provider.get(PC).ports.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('lazy initialization', () => {
|
||||
const framework = new Framework();
|
||||
interface Command {
|
||||
shortcut: string;
|
||||
callback: () => void;
|
||||
}
|
||||
const Command = createIdentifier<Command>('command');
|
||||
|
||||
let pageSystemInitialized = false;
|
||||
|
||||
class PageSystem extends Service {
|
||||
mode = 'page';
|
||||
name = 'helloworld';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
pageSystemInitialized = true;
|
||||
}
|
||||
|
||||
switchToEdgeless() {
|
||||
this.mode = 'edgeless';
|
||||
}
|
||||
|
||||
rename() {
|
||||
this.name = 'foobar';
|
||||
}
|
||||
}
|
||||
|
||||
class CommandSystem extends Service {
|
||||
constructor(public commands: Command[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(shortcut: string) {
|
||||
const command = this.commands.find(c => c.shortcut === shortcut);
|
||||
if (command) {
|
||||
command.callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(PageSystem);
|
||||
framework.service(CommandSystem, [[Command]]);
|
||||
framework.impl(Command('switch'), p => ({
|
||||
shortcut: 'option+s',
|
||||
callback: () => p.get(PageSystem).switchToEdgeless(),
|
||||
}));
|
||||
framework.impl(Command('rename'), p => ({
|
||||
shortcut: 'f2',
|
||||
callback: () => p.get(PageSystem).rename(),
|
||||
}));
|
||||
|
||||
const provider = framework.provider();
|
||||
const commandSystem = provider.get(CommandSystem);
|
||||
|
||||
expect(
|
||||
pageSystemInitialized,
|
||||
"PageSystem won't be initialized until command executed"
|
||||
).toEqual(false);
|
||||
|
||||
commandSystem.execute('option+s');
|
||||
expect(pageSystemInitialized).toEqual(true);
|
||||
expect(provider.get(PageSystem).mode).toEqual('edgeless');
|
||||
|
||||
expect(provider.get(PageSystem).name).toEqual('helloworld');
|
||||
expect(commandSystem.commands.length).toEqual(2);
|
||||
commandSystem.execute('f2');
|
||||
expect(provider.get(PageSystem).name).toEqual('foobar');
|
||||
});
|
||||
|
||||
test('duplicate, override', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
const something = createIdentifier<any>('USB');
|
||||
|
||||
class A {
|
||||
a = 'i am A';
|
||||
}
|
||||
|
||||
class B {
|
||||
b = 'i am B';
|
||||
}
|
||||
|
||||
framework.impl(something, A).override(something, B);
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(provider.get(something)).toEqual({ b: 'i am B' });
|
||||
});
|
||||
|
||||
test('event', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
const event = createEvent<{ value: number }>('test-event');
|
||||
|
||||
@OnEvent(event, p => p.onTestEvent)
|
||||
class TestService extends Service {
|
||||
value = 0;
|
||||
|
||||
onTestEvent(payload: { value: number }) {
|
||||
this.value = payload.value;
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(TestService);
|
||||
|
||||
const provider = framework.provider();
|
||||
provider.emitEvent(event, { value: 123 });
|
||||
expect(provider.get(TestService).value).toEqual(123);
|
||||
});
|
||||
|
||||
test('scope', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
class SystemService extends Service {
|
||||
appName = 'affine';
|
||||
}
|
||||
|
||||
framework.service(SystemService);
|
||||
|
||||
class WorkspaceScope extends Scope {}
|
||||
|
||||
class WorkspaceService extends Service {
|
||||
constructor(public system: SystemService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework.scope(WorkspaceScope).service(WorkspaceService, [SystemService]);
|
||||
|
||||
class PageScope extends Scope<{ pageId: string }> {}
|
||||
|
||||
class PageService extends Service {
|
||||
constructor(
|
||||
public workspace: WorkspaceService,
|
||||
public system: SystemService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.scope(PageScope)
|
||||
.service(PageService, [WorkspaceService, SystemService]);
|
||||
|
||||
class EditorScope extends Scope {
|
||||
get pageId() {
|
||||
return this.framework.get(PageScope).props.pageId;
|
||||
}
|
||||
}
|
||||
|
||||
class EditorService extends Service {
|
||||
constructor(public page: PageService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.scope(PageScope)
|
||||
.scope(EditorScope)
|
||||
.service(EditorService, [PageService]);
|
||||
|
||||
const root = framework.provider();
|
||||
expect(root.get(SystemService).appName).toEqual('affine');
|
||||
expect(() => root.get(WorkspaceService)).toThrowError(
|
||||
ComponentNotFoundError
|
||||
);
|
||||
|
||||
const workspaceScope = root.createScope(WorkspaceScope);
|
||||
const workspaceService = workspaceScope.get(WorkspaceService);
|
||||
expect(workspaceService.system.appName).toEqual('affine');
|
||||
expect(() => workspaceScope.get(PageService)).toThrowError(
|
||||
ComponentNotFoundError
|
||||
);
|
||||
|
||||
const pageScope = workspaceScope.createScope(PageScope, {
|
||||
pageId: 'test-page',
|
||||
});
|
||||
expect(pageScope.props.pageId).toEqual('test-page');
|
||||
const pageService = pageScope.get(PageService);
|
||||
expect(pageService.workspace).toBe(workspaceService);
|
||||
expect(pageService.system.appName).toEqual('affine');
|
||||
|
||||
const editorScope = pageScope.createScope(EditorScope);
|
||||
expect(editorScope.pageId).toEqual('test-page');
|
||||
const editorService = editorScope.get(EditorService);
|
||||
expect(editorService.page).toBe(pageService);
|
||||
});
|
||||
|
||||
test('scope event', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
const event = createEvent<{ value: number }>('test-event');
|
||||
|
||||
@OnEvent(event, p => p.onTestEvent)
|
||||
class TestService extends Service {
|
||||
value = 0;
|
||||
|
||||
onTestEvent(payload: { value: number }) {
|
||||
this.value = payload.value;
|
||||
}
|
||||
}
|
||||
|
||||
class TestScope extends Scope {}
|
||||
|
||||
@OnEvent(event, p => p.onTestEvent)
|
||||
class TestScopeService extends Service {
|
||||
value = 0;
|
||||
|
||||
onTestEvent(payload: { value: number }) {
|
||||
this.value = payload.value;
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(TestService).scope(TestScope).service(TestScopeService);
|
||||
|
||||
const provider = framework.provider();
|
||||
const scope = provider.createScope(TestScope);
|
||||
scope.emitEvent(event, { value: 123 });
|
||||
expect(provider.get(TestService).value).toEqual(0);
|
||||
expect(scope.get(TestScopeService).value).toEqual(123);
|
||||
});
|
||||
|
||||
test('dispose', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
let isSystemDisposed = false;
|
||||
class System extends Service {
|
||||
appName = 'affine';
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
isSystemDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(System);
|
||||
|
||||
let isWorkspaceDisposed = false;
|
||||
class WorkspaceScope extends Scope {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
isWorkspaceDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let isWorkspacePageServiceDisposed = false;
|
||||
class WorkspacePageService extends Service {
|
||||
constructor(
|
||||
public workspace: WorkspaceScope,
|
||||
public sysmte: System
|
||||
) {
|
||||
super();
|
||||
}
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
isWorkspacePageServiceDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspacePageService, [WorkspaceScope, System]);
|
||||
|
||||
{
|
||||
using root = framework.provider();
|
||||
|
||||
{
|
||||
// create a workspace
|
||||
using workspaceScope = root.createScope(WorkspaceScope);
|
||||
const pageService = workspaceScope.get(WorkspacePageService);
|
||||
|
||||
expect(pageService).instanceOf(WorkspacePageService);
|
||||
|
||||
expect(
|
||||
isSystemDisposed ||
|
||||
isWorkspaceDisposed ||
|
||||
isWorkspacePageServiceDisposed
|
||||
).toBe(false);
|
||||
}
|
||||
expect(isWorkspaceDisposed && isWorkspacePageServiceDisposed).toBe(true);
|
||||
|
||||
expect(isSystemDisposed).toBe(false);
|
||||
}
|
||||
expect(isSystemDisposed).toBe(true);
|
||||
});
|
||||
|
||||
test('service not found', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(() => provider.get(createIdentifier('SomeService'))).toThrowError(
|
||||
ComponentNotFoundError
|
||||
);
|
||||
});
|
||||
|
||||
test('missing dependency', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
class A extends Service {
|
||||
value = 'hello world';
|
||||
}
|
||||
|
||||
class B extends Service {
|
||||
constructor(public a: A) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(B, [A]);
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(() => provider.get(B)).toThrowError(MissingDependencyError);
|
||||
});
|
||||
|
||||
test('circular dependency', () => {
|
||||
const framework = new Framework();
|
||||
|
||||
class A extends Service {
|
||||
constructor(public c: C) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends Service {
|
||||
constructor(public a: A) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class C extends Service {
|
||||
constructor(public b: B) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
framework.service(A, [C]).service(B, [A]).service(C, [B]);
|
||||
|
||||
const provider = framework.provider();
|
||||
expect(() => provider.get(A)).toThrowError(CircularDependencyError);
|
||||
expect(() => provider.get(B)).toThrowError(CircularDependencyError);
|
||||
expect(() => provider.get(C)).toThrowError(CircularDependencyError);
|
||||
});
|
||||
|
||||
test('duplicate service definition', () => {
|
||||
const serviceCollection = new Framework();
|
||||
|
||||
class A extends Service {}
|
||||
|
||||
serviceCollection.service(A);
|
||||
expect(() => serviceCollection.service(A)).toThrowError(
|
||||
DuplicateDefinitionError
|
||||
);
|
||||
|
||||
class B {}
|
||||
const Something = createIdentifier('something');
|
||||
serviceCollection.impl(Something, A);
|
||||
expect(() => serviceCollection.impl(Something, B)).toThrowError(
|
||||
DuplicateDefinitionError
|
||||
);
|
||||
});
|
||||
|
||||
test('recursion limit', () => {
|
||||
// maxmium resolve depth is 100
|
||||
const serviceCollection = new Framework();
|
||||
const Something = createIdentifier('something');
|
||||
let i = 0;
|
||||
for (; i < 100; i++) {
|
||||
const next = i + 1;
|
||||
|
||||
class Test {
|
||||
constructor(_next: any) {}
|
||||
}
|
||||
|
||||
serviceCollection.impl(Something(i.toString()), Test, [
|
||||
Something(next.toString()),
|
||||
]);
|
||||
}
|
||||
|
||||
class Final {
|
||||
a = 'b';
|
||||
}
|
||||
serviceCollection.impl(Something(i.toString()), Final);
|
||||
const provider = serviceCollection.provider();
|
||||
expect(() => provider.get(Something('0'))).toThrowError(
|
||||
RecursionLimitError
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CONSTRUCTOR_CONTEXT } from '../constructor-context';
|
||||
import type { FrameworkProvider } from '../provider';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export class Component<Props = {}> {
|
||||
readonly framework: FrameworkProvider;
|
||||
readonly props: Props;
|
||||
|
||||
get eventBus() {
|
||||
return this.framework.eventBus;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (!CONSTRUCTOR_CONTEXT.current.provider) {
|
||||
throw new Error('Component must be created in the context of a provider');
|
||||
}
|
||||
this.framework = CONSTRUCTOR_CONTEXT.current.provider;
|
||||
this.props = CONSTRUCTOR_CONTEXT.current.props;
|
||||
CONSTRUCTOR_CONTEXT.current = {};
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Component } from './component';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export class Entity<Props = {}> extends Component<Props> {
|
||||
readonly __isEntity = true;
|
||||
}
|
||||
43
packages/common/infra/src/framework/core/components/scope.ts
Normal file
43
packages/common/infra/src/framework/core/components/scope.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Component } from './component';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export class Scope<Props = {}> extends Component<Props> {
|
||||
readonly __injectable = true;
|
||||
|
||||
get collection() {
|
||||
return this.framework.collection;
|
||||
}
|
||||
|
||||
get scope() {
|
||||
return this.framework.scope;
|
||||
}
|
||||
|
||||
get get() {
|
||||
return this.framework.get;
|
||||
}
|
||||
|
||||
get getAll() {
|
||||
return this.framework.getAll;
|
||||
}
|
||||
|
||||
get getOptional() {
|
||||
return this.framework.getOptional;
|
||||
}
|
||||
|
||||
get createEntity() {
|
||||
return this.framework.createEntity;
|
||||
}
|
||||
|
||||
get createScope() {
|
||||
return this.framework.createScope;
|
||||
}
|
||||
|
||||
get emitEvent() {
|
||||
return this.framework.emitEvent;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.framework.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Component } from './component';
|
||||
|
||||
export class Service extends Component {
|
||||
readonly __isService = true;
|
||||
readonly __injectable = true;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Component } from './component';
|
||||
|
||||
export class Store extends Component {
|
||||
readonly __isStore = true;
|
||||
readonly __injectable = true;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { FrameworkProvider } from './provider';
|
||||
|
||||
interface Context {
|
||||
provider?: FrameworkProvider;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
export const CONSTRUCTOR_CONTEXT: {
|
||||
current: Context;
|
||||
} = { current: {} };
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function withContext<T>(cb: () => T, context: Context): T {
|
||||
const pre = CONSTRUCTOR_CONTEXT.current;
|
||||
try {
|
||||
CONSTRUCTOR_CONTEXT.current = context;
|
||||
return cb();
|
||||
} finally {
|
||||
CONSTRUCTOR_CONTEXT.current = pre;
|
||||
}
|
||||
}
|
||||
6
packages/common/infra/src/framework/core/consts.ts
Normal file
6
packages/common/infra/src/framework/core/consts.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ComponentVariant } from './types';
|
||||
|
||||
export const DEFAULT_VARIANT: ComponentVariant = 'default';
|
||||
export const ROOT_SCOPE = [];
|
||||
|
||||
export const SUB_COMPONENTS = Symbol('subComponents');
|
||||
59
packages/common/infra/src/framework/core/error.ts
Normal file
59
packages/common/infra/src/framework/core/error.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { DEFAULT_VARIANT } from './consts';
|
||||
import type { IdentifierValue } from './types';
|
||||
|
||||
export class RecursionLimitError extends Error {
|
||||
constructor() {
|
||||
super('Dynamic resolve recursion limit reached');
|
||||
}
|
||||
}
|
||||
|
||||
export class CircularDependencyError extends Error {
|
||||
constructor(public readonly dependencyStack: IdentifierValue[]) {
|
||||
super(
|
||||
`A circular dependency was detected.\n` +
|
||||
stringifyDependencyStack(dependencyStack)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentNotFoundError extends Error {
|
||||
constructor(public readonly identifier: IdentifierValue) {
|
||||
super(
|
||||
`Component ${stringifyIdentifier(identifier)} not found in container`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingDependencyError extends Error {
|
||||
constructor(
|
||||
public readonly from: IdentifierValue,
|
||||
public readonly target: IdentifierValue,
|
||||
public readonly dependencyStack: IdentifierValue[]
|
||||
) {
|
||||
super(
|
||||
`Missing dependency ${stringifyIdentifier(
|
||||
target
|
||||
)} in creating ${stringifyIdentifier(
|
||||
from
|
||||
)}.\n${stringifyDependencyStack(dependencyStack)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateDefinitionError extends Error {
|
||||
constructor(public readonly identifier: IdentifierValue) {
|
||||
super(`${stringifyIdentifier(identifier)} already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyIdentifier(identifier: IdentifierValue) {
|
||||
return `[${identifier.identifierName}]${
|
||||
identifier.variant !== DEFAULT_VARIANT ? `(${identifier.variant})` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
function stringifyDependencyStack(dependencyStack: IdentifierValue[]) {
|
||||
return dependencyStack
|
||||
.map(identifier => `${stringifyIdentifier(identifier)}`)
|
||||
.join(' -> ');
|
||||
}
|
||||
111
packages/common/infra/src/framework/core/event.ts
Normal file
111
packages/common/infra/src/framework/core/event.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
import { stableHash } from '../../utils';
|
||||
import type { FrameworkProvider } from '.';
|
||||
import type { Service } from './components/service';
|
||||
import { SUB_COMPONENTS } from './consts';
|
||||
import { createIdentifier } from './identifier';
|
||||
import type { SubComponent } from './types';
|
||||
|
||||
export interface FrameworkEvent<T> {
|
||||
id: string;
|
||||
_type: T;
|
||||
}
|
||||
|
||||
export function createEvent<T>(id: string): FrameworkEvent<T> {
|
||||
return { id, _type: {} as T };
|
||||
}
|
||||
|
||||
export type FrameworkEventType<T> =
|
||||
T extends FrameworkEvent<infer E> ? E : never;
|
||||
|
||||
const logger = new DebugLogger('affine:event-bus');
|
||||
|
||||
export class EventBus {
|
||||
private listeners: Record<string, Array<(payload: any) => void>> = {};
|
||||
|
||||
constructor(
|
||||
provider: FrameworkProvider,
|
||||
private readonly parent?: EventBus
|
||||
) {
|
||||
const handlers = provider.getAll(EventHandler, {
|
||||
sameScope: true,
|
||||
});
|
||||
|
||||
for (const handler of handlers.values()) {
|
||||
this.on(handler.event.id, handler.handler);
|
||||
}
|
||||
}
|
||||
|
||||
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
||||
if (!this.listeners[id]) {
|
||||
this.listeners[id] = [];
|
||||
}
|
||||
this.listeners[id].push(listener);
|
||||
const off = this.parent?.on(id, listener);
|
||||
return () => {
|
||||
this.off(id, listener);
|
||||
off?.();
|
||||
};
|
||||
}
|
||||
|
||||
off<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
||||
if (!this.listeners[id]) {
|
||||
return;
|
||||
}
|
||||
this.listeners[id] = this.listeners[id].filter(l => l !== listener);
|
||||
}
|
||||
|
||||
emit<T>(event: FrameworkEvent<T>, payload: T) {
|
||||
logger.debug('Emitting event', event.id, payload);
|
||||
const listeners = this.listeners[event.id];
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
listeners.forEach(listener => {
|
||||
try {
|
||||
listener(payload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface EventHandler {
|
||||
event: FrameworkEvent<any>;
|
||||
handler: (payload: any) => void;
|
||||
}
|
||||
|
||||
export const EventHandler = createIdentifier<EventHandler>('EventHandler');
|
||||
|
||||
export const OnEvent = <
|
||||
E extends FrameworkEvent<any>,
|
||||
C extends abstract new (...args: any) => any,
|
||||
I = InstanceType<C>,
|
||||
>(
|
||||
e: E,
|
||||
pick: I extends Service ? (i: I) => (e: FrameworkEventType<E>) => void : never
|
||||
) => {
|
||||
return (target: C): C => {
|
||||
const handlers = (target as any)[SUB_COMPONENTS] ?? [];
|
||||
(target as any)[SUB_COMPONENTS] = [
|
||||
...handlers,
|
||||
{
|
||||
identifier: EventHandler(
|
||||
target.name + stableHash(e) + stableHash(pick)
|
||||
),
|
||||
factory: provider => {
|
||||
return {
|
||||
event: e,
|
||||
handler: (payload: any) => {
|
||||
const i = provider.get(target);
|
||||
pick(i).apply(i, [payload]);
|
||||
},
|
||||
} satisfies EventHandler;
|
||||
},
|
||||
} satisfies SubComponent,
|
||||
];
|
||||
return target;
|
||||
};
|
||||
};
|
||||
527
packages/common/infra/src/framework/core/framework.ts
Normal file
527
packages/common/infra/src/framework/core/framework.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import type { Component } from './components/component';
|
||||
import type { Entity } from './components/entity';
|
||||
import type { Scope } from './components/scope';
|
||||
import type { Service } from './components/service';
|
||||
import type { Store } from './components/store';
|
||||
import { DEFAULT_VARIANT, ROOT_SCOPE, SUB_COMPONENTS } from './consts';
|
||||
import { DuplicateDefinitionError } from './error';
|
||||
import { parseIdentifier } from './identifier';
|
||||
import type { FrameworkProvider } from './provider';
|
||||
import { BasicFrameworkProvider } from './provider';
|
||||
import { stringifyScope } from './scope';
|
||||
import type {
|
||||
ComponentFactory,
|
||||
ComponentVariant,
|
||||
FrameworkScopeStack,
|
||||
GeneralIdentifier,
|
||||
Identifier,
|
||||
IdentifierType,
|
||||
IdentifierValue,
|
||||
SubComponent,
|
||||
Type,
|
||||
TypesToDeps,
|
||||
} from './types';
|
||||
|
||||
export class Framework {
|
||||
private readonly components: Map<
|
||||
string,
|
||||
Map<string, Map<ComponentVariant, ComponentFactory>>
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
* Create an empty framework.
|
||||
*
|
||||
* same as `new Framework()`
|
||||
*/
|
||||
static get EMPTY() {
|
||||
return new Framework();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of components in the framework.
|
||||
*/
|
||||
get componentCount() {
|
||||
let count = 0;
|
||||
for (const [, identifiers] of this.components) {
|
||||
for (const [, variants] of identifiers) {
|
||||
count += variants.size;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.service}
|
||||
*/
|
||||
get service() {
|
||||
return new FrameworkEditor(this).service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.impl}
|
||||
*/
|
||||
get impl() {
|
||||
return new FrameworkEditor(this).impl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.entity}
|
||||
*/
|
||||
get entity() {
|
||||
return new FrameworkEditor(this).entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.scope}
|
||||
*/
|
||||
get scope() {
|
||||
return new FrameworkEditor(this).scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.override}
|
||||
*/
|
||||
get override() {
|
||||
return new FrameworkEditor(this).override;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link FrameworkEditor.store}
|
||||
*/
|
||||
get store() {
|
||||
return new FrameworkEditor(this).store;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Use {@link impl} instead.
|
||||
*/
|
||||
addValue<T>(
|
||||
identifier: GeneralIdentifier<T>,
|
||||
value: T,
|
||||
{
|
||||
scope,
|
||||
override,
|
||||
}: { scope?: FrameworkScopeStack; override?: boolean } = {}
|
||||
) {
|
||||
this.addFactory(parseIdentifier(identifier) as Identifier<T>, () => value, {
|
||||
scope,
|
||||
override,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Use {@link impl} instead.
|
||||
*/
|
||||
addFactory<T>(
|
||||
identifier: GeneralIdentifier<T>,
|
||||
factory: ComponentFactory<T>,
|
||||
{
|
||||
scope,
|
||||
override,
|
||||
}: { scope?: FrameworkScopeStack; override?: boolean } = {}
|
||||
) {
|
||||
// convert scope to string
|
||||
const normalizedScope = stringifyScope(scope ?? ROOT_SCOPE);
|
||||
const normalizedIdentifier = parseIdentifier(identifier);
|
||||
const normalizedVariant = normalizedIdentifier.variant ?? DEFAULT_VARIANT;
|
||||
|
||||
const services =
|
||||
this.components.get(normalizedScope) ??
|
||||
new Map<string, Map<ComponentVariant, ComponentFactory>>();
|
||||
|
||||
const variants =
|
||||
services.get(normalizedIdentifier.identifierName) ??
|
||||
new Map<ComponentVariant, ComponentFactory>();
|
||||
|
||||
// throw if service already exists, unless it is an override
|
||||
if (variants.has(normalizedVariant) && !override) {
|
||||
throw new DuplicateDefinitionError(normalizedIdentifier);
|
||||
}
|
||||
variants.set(normalizedVariant, factory);
|
||||
services.set(normalizedIdentifier.identifierName, variants);
|
||||
this.components.set(normalizedScope, services);
|
||||
}
|
||||
|
||||
remove(identifier: IdentifierValue, scope: FrameworkScopeStack = ROOT_SCOPE) {
|
||||
const normalizedScope = stringifyScope(scope);
|
||||
const normalizedIdentifier = parseIdentifier(identifier);
|
||||
const normalizedVariant = normalizedIdentifier.variant ?? DEFAULT_VARIANT;
|
||||
|
||||
const services = this.components.get(normalizedScope);
|
||||
if (!services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variants = services.get(normalizedIdentifier.identifierName);
|
||||
if (!variants) {
|
||||
return;
|
||||
}
|
||||
|
||||
variants.delete(normalizedVariant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a service provider from the collection.
|
||||
*
|
||||
* @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: FrameworkScopeStack = ROOT_SCOPE,
|
||||
parent: FrameworkProvider | null = null
|
||||
): FrameworkProvider {
|
||||
return new BasicFrameworkProvider(this, scope, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getFactory(
|
||||
identifier: IdentifierValue,
|
||||
scope: FrameworkScopeStack = ROOT_SCOPE
|
||||
): ComponentFactory | undefined {
|
||||
return this.components
|
||||
.get(stringifyScope(scope))
|
||||
?.get(identifier.identifierName)
|
||||
?.get(identifier.variant ?? DEFAULT_VARIANT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getFactoryAll(
|
||||
identifier: IdentifierValue,
|
||||
scope: FrameworkScopeStack = ROOT_SCOPE
|
||||
): Map<ComponentVariant, ComponentFactory> {
|
||||
return new Map(
|
||||
this.components.get(stringifyScope(scope))?.get(identifier.identifierName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the entire service collection.
|
||||
*
|
||||
* This method is quite cheap as it only clones the references.
|
||||
*
|
||||
* @returns A new service collection with the same services.
|
||||
*/
|
||||
clone(): Framework {
|
||||
const di = new Framework();
|
||||
for (const [scope, identifiers] of this.components) {
|
||||
const s = new Map();
|
||||
for (const [identifier, variants] of identifiers) {
|
||||
s.set(identifier, new Map(variants));
|
||||
}
|
||||
di.components.set(scope, s);
|
||||
}
|
||||
return di;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to edit a framework.
|
||||
*/
|
||||
class FrameworkEditor {
|
||||
private currentScopeStack: FrameworkScopeStack = ROOT_SCOPE;
|
||||
|
||||
constructor(private readonly collection: Framework) {}
|
||||
|
||||
/**
|
||||
* Add a service to the framework.
|
||||
*
|
||||
* @see {@link Framework}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* service(ServiceClass, [dependencies, ...])
|
||||
* ```
|
||||
*/
|
||||
service = <
|
||||
Arg1 extends Type<Service>,
|
||||
Arg2 extends Deps | ComponentFactory<ServiceType> | ServiceType,
|
||||
ServiceType = IdentifierType<Arg1>,
|
||||
Deps = Arg1 extends Type<ServiceType>
|
||||
? TypesToDeps<ConstructorParameters<Arg1>>
|
||||
: [],
|
||||
>(
|
||||
service: Arg1,
|
||||
...[arg2]: Arg2 extends [] ? [] : [Arg2]
|
||||
): this => {
|
||||
if (arg2 instanceof Function) {
|
||||
this.collection.addFactory<any>(service as any, arg2 as any, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
} else if (arg2 instanceof Array || arg2 === undefined) {
|
||||
this.collection.addFactory<any>(
|
||||
service as any,
|
||||
dependenciesToFactory(service, arg2 as any),
|
||||
{ scope: this.currentScopeStack }
|
||||
);
|
||||
} else {
|
||||
this.collection.addValue<any>(service as any, arg2, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
}
|
||||
|
||||
if (SUB_COMPONENTS in service) {
|
||||
const subComponents = (service as any)[SUB_COMPONENTS] as SubComponent[];
|
||||
for (const { identifier, factory } of subComponents) {
|
||||
this.collection.addFactory(identifier, factory, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a store to the framework.
|
||||
*
|
||||
* @see {@link Framework}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* store(StoreClass, [dependencies, ...])
|
||||
* ```
|
||||
*/
|
||||
store = <
|
||||
Arg1 extends Type<Store>,
|
||||
Arg2 extends Deps | ComponentFactory<StoreType> | StoreType,
|
||||
StoreType = IdentifierType<Arg1>,
|
||||
Deps = Arg1 extends Type<StoreType>
|
||||
? TypesToDeps<ConstructorParameters<Arg1>>
|
||||
: [],
|
||||
>(
|
||||
store: Arg1,
|
||||
...[arg2]: Arg2 extends [] ? [] : [Arg2]
|
||||
): this => {
|
||||
if (arg2 instanceof Function) {
|
||||
this.collection.addFactory<any>(store as any, arg2 as any, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
} else if (arg2 instanceof Array || arg2 === undefined) {
|
||||
this.collection.addFactory<any>(
|
||||
store as any,
|
||||
dependenciesToFactory(store, arg2 as any),
|
||||
{ scope: this.currentScopeStack }
|
||||
);
|
||||
} else {
|
||||
this.collection.addValue<any>(store as any, arg2, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
}
|
||||
|
||||
if (SUB_COMPONENTS in store) {
|
||||
const subComponents = (store as any)[SUB_COMPONENTS] as SubComponent[];
|
||||
for (const { identifier, factory } of subComponents) {
|
||||
this.collection.addFactory(identifier, factory, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an entity to the framework.
|
||||
*/
|
||||
entity = <
|
||||
Arg1 extends Type<Entity<any>>,
|
||||
Arg2 extends Deps | ComponentFactory<EntityType>,
|
||||
EntityType = IdentifierType<Arg1>,
|
||||
Deps = Arg1 extends Type<EntityType>
|
||||
? TypesToDeps<ConstructorParameters<Arg1>>
|
||||
: [],
|
||||
>(
|
||||
entity: Arg1,
|
||||
...[arg2]: Arg2 extends [] ? [] : [Arg2]
|
||||
): this => {
|
||||
if (arg2 instanceof Function) {
|
||||
this.collection.addFactory<any>(entity as any, arg2 as any, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
} else {
|
||||
this.collection.addFactory<any>(
|
||||
entity as any,
|
||||
dependenciesToFactory(entity, arg2 as any),
|
||||
{ scope: this.currentScopeStack }
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an implementation for identifier to the collection.
|
||||
*
|
||||
* @see {@link Framework}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* addImpl(Identifier, Class, [dependencies, ...])
|
||||
* or
|
||||
* addImpl(Identifier, Instance)
|
||||
* or
|
||||
* addImpl(Identifier, Factory)
|
||||
* ```
|
||||
*/
|
||||
impl = <
|
||||
Arg1 extends Identifier<any>,
|
||||
Arg2 extends Type<Trait> | ComponentFactory<Trait> | Trait,
|
||||
Arg3 extends Deps,
|
||||
Trait = IdentifierType<Arg1>,
|
||||
Deps = Arg2 extends Type<Trait>
|
||||
? TypesToDeps<ConstructorParameters<Arg2>>
|
||||
: [],
|
||||
>(
|
||||
identifier: Arg1,
|
||||
arg2: Arg2,
|
||||
...[arg3]: Arg3 extends [] ? [] : [Arg3]
|
||||
): this => {
|
||||
if (arg2 instanceof Function) {
|
||||
this.collection.addFactory<any>(
|
||||
identifier,
|
||||
dependenciesToFactory(arg2, arg3 as any[]),
|
||||
{ scope: this.currentScopeStack }
|
||||
);
|
||||
} else {
|
||||
this.collection.addValue(identifier, arg2 as any, {
|
||||
scope: this.currentScopeStack,
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* same as {@link impl} but this method will override the component if it exists.
|
||||
*
|
||||
* @see {@link Framework}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* override(OriginClass, NewClass, [dependencies, ...])
|
||||
* or
|
||||
* override(Identifier, Class, [dependencies, ...])
|
||||
* or
|
||||
* override(Identifier, Instance)
|
||||
* or
|
||||
* override(Identifier, Factory)
|
||||
* ```
|
||||
*/
|
||||
override = <
|
||||
Arg1 extends GeneralIdentifier<any>,
|
||||
Arg2 extends Type<Trait> | ComponentFactory<Trait> | Trait | null,
|
||||
Arg3 extends Deps,
|
||||
Trait extends Component = IdentifierType<Arg1>,
|
||||
Deps = Arg2 extends Type<Trait>
|
||||
? TypesToDeps<ConstructorParameters<Arg2>>
|
||||
: [],
|
||||
>(
|
||||
identifier: Arg1,
|
||||
arg2: Arg2,
|
||||
...[arg3]: Arg3 extends [] ? [] : [Arg3]
|
||||
): this => {
|
||||
if (arg2 === null) {
|
||||
this.collection.remove(
|
||||
parseIdentifier(identifier),
|
||||
this.currentScopeStack
|
||||
);
|
||||
return this;
|
||||
} else if (arg2 instanceof Function) {
|
||||
this.collection.addFactory<any>(
|
||||
identifier,
|
||||
dependenciesToFactory(arg2, arg3 as any[]),
|
||||
{ scope: this.currentScopeStack, override: true }
|
||||
);
|
||||
} else {
|
||||
this.collection.addValue(identifier, arg2 as any, {
|
||||
scope: this.currentScopeStack,
|
||||
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: Type<Scope<any>>): this => {
|
||||
this.currentScopeStack = [
|
||||
...this.currentScopeStack,
|
||||
parseIdentifier(scope).identifierName,
|
||||
];
|
||||
|
||||
this.collection.addFactory<any>(
|
||||
scope as any,
|
||||
dependenciesToFactory(scope, [] as any),
|
||||
{ scope: this.currentScopeStack, override: true }
|
||||
);
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dependencies definition to a factory function.
|
||||
*/
|
||||
function dependenciesToFactory(
|
||||
cls: any,
|
||||
deps: any[] = []
|
||||
): ComponentFactory<any> {
|
||||
return (provider: FrameworkProvider) => {
|
||||
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: any) {
|
||||
try {
|
||||
Reflect.construct(function () {}, [], cls);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
114
packages/common/infra/src/framework/core/identifier.ts
Normal file
114
packages/common/infra/src/framework/core/identifier.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { stableHash } from '../../utils/stable-hash';
|
||||
import type { Component } from './components/component';
|
||||
import { DEFAULT_VARIANT } from './consts';
|
||||
import type {
|
||||
ComponentVariant,
|
||||
Identifier,
|
||||
IdentifierValue,
|
||||
Type,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* create a Identifier.
|
||||
*
|
||||
* Identifier 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
|
||||
* framework.impl(Storage, LocalStorage);
|
||||
*
|
||||
* // get the implementation from the identifier
|
||||
* const storage = framework.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');
|
||||
*
|
||||
* framework.impl(LocalStorage, LocalStorageImpl);
|
||||
* framework.impl(SessionStorage, SessionStorageImpl);
|
||||
*
|
||||
* // get the implementation from the identifier
|
||||
* const localStorage = framework.provider().get(LocalStorage);
|
||||
* const sessionStorage = framework.provider().get(SessionStorage);
|
||||
* const storage = framework.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: ComponentVariant = DEFAULT_VARIANT
|
||||
): Identifier<T> & ((variant: ComponentVariant) => Identifier<T>) {
|
||||
return Object.assign(
|
||||
(variant: ComponentVariant) => {
|
||||
return createIdentifier<T>(name, variant);
|
||||
},
|
||||
{
|
||||
identifierName: name,
|
||||
variant,
|
||||
}
|
||||
) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the constructor into a ServiceIdentifier.
|
||||
* As we always deal with ServiceIdentifier in the DI container.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function createIdentifierFromConstructor<T extends Component>(
|
||||
target: Type<T>
|
||||
): Identifier<T> {
|
||||
return createIdentifier<T>(`${target.name}${stableHash(target)}`);
|
||||
}
|
||||
|
||||
export function parseIdentifier(input: any): IdentifierValue {
|
||||
if (input.identifierName) {
|
||||
return input as IdentifierValue;
|
||||
} else if (typeof input === 'function' && input.name) {
|
||||
return createIdentifierFromConstructor(input);
|
||||
} else {
|
||||
throw new Error('Input is not a service identifier.');
|
||||
}
|
||||
}
|
||||
10
packages/common/infra/src/framework/core/index.ts
Normal file
10
packages/common/infra/src/framework/core/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { Entity } from './components/entity';
|
||||
export { Scope } from './components/scope';
|
||||
export { Service } from './components/service';
|
||||
export { Store } from './components/store';
|
||||
export * from './error';
|
||||
export { createEvent, OnEvent } from './event';
|
||||
export { Framework } from './framework';
|
||||
export { createIdentifier } from './identifier';
|
||||
export type { FrameworkProvider, ResolveOptions } from './provider';
|
||||
export type { GeneralIdentifier } from './types';
|
||||
321
packages/common/infra/src/framework/core/provider.ts
Normal file
321
packages/common/infra/src/framework/core/provider.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import type { Component } from './components/component';
|
||||
import type { Entity } from './components/entity';
|
||||
import type { Scope } from './components/scope';
|
||||
import { withContext } from './constructor-context';
|
||||
import {
|
||||
CircularDependencyError,
|
||||
ComponentNotFoundError,
|
||||
MissingDependencyError,
|
||||
RecursionLimitError,
|
||||
} from './error';
|
||||
import { EventBus, type FrameworkEvent } from './event';
|
||||
import type { Framework } from './framework';
|
||||
import { parseIdentifier } from './identifier';
|
||||
import type {
|
||||
ComponentVariant,
|
||||
FrameworkScopeStack,
|
||||
GeneralIdentifier,
|
||||
IdentifierValue,
|
||||
} from './types';
|
||||
|
||||
export interface ResolveOptions {
|
||||
sameScope?: boolean;
|
||||
optional?: boolean;
|
||||
noCache?: boolean;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
export abstract class FrameworkProvider {
|
||||
abstract collection: Framework;
|
||||
abstract scope: FrameworkScopeStack;
|
||||
abstract getRaw(identifier: IdentifierValue, options?: ResolveOptions): any;
|
||||
abstract getAllRaw(
|
||||
identifier: IdentifierValue,
|
||||
options?: ResolveOptions
|
||||
): Map<ComponentVariant, any>;
|
||||
abstract dispose(): void;
|
||||
abstract eventBus: EventBus;
|
||||
|
||||
get = <T>(identifier: GeneralIdentifier<T>, options?: ResolveOptions): T => {
|
||||
return this.getRaw(parseIdentifier(identifier), {
|
||||
...options,
|
||||
optional: false,
|
||||
});
|
||||
};
|
||||
|
||||
getAll = <T>(
|
||||
identifier: GeneralIdentifier<T>,
|
||||
options?: ResolveOptions
|
||||
): Map<ComponentVariant, T> => {
|
||||
return this.getAllRaw(parseIdentifier(identifier), {
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
getOptional = <T>(
|
||||
identifier: GeneralIdentifier<T>,
|
||||
options?: ResolveOptions
|
||||
): T | null => {
|
||||
return this.getRaw(parseIdentifier(identifier), {
|
||||
...options,
|
||||
optional: true,
|
||||
});
|
||||
};
|
||||
|
||||
createEntity = <
|
||||
T extends Entity<any>,
|
||||
Props extends T extends Component<infer P> ? P : never,
|
||||
>(
|
||||
identifier: GeneralIdentifier<T>,
|
||||
...[props]: Props extends Record<string, never> ? [] : [Props]
|
||||
): T => {
|
||||
return this.getRaw(parseIdentifier(identifier), {
|
||||
noCache: true,
|
||||
sameScope: true,
|
||||
props,
|
||||
});
|
||||
};
|
||||
|
||||
createScope = <
|
||||
T extends Scope<any>,
|
||||
Props extends T extends Component<infer P> ? P : never,
|
||||
>(
|
||||
root: GeneralIdentifier<T>,
|
||||
...[props]: Props extends Record<string, never> ? [] : [Props]
|
||||
): T => {
|
||||
const newProvider = this.collection.provider(
|
||||
[...this.scope, parseIdentifier(root).identifierName],
|
||||
this
|
||||
);
|
||||
return newProvider.getRaw(parseIdentifier(root), {
|
||||
sameScope: true,
|
||||
props,
|
||||
});
|
||||
};
|
||||
|
||||
emitEvent = <T>(event: FrameworkEvent<T>, payload: T) => {
|
||||
this.eventBus.emit(event, payload);
|
||||
};
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentCachePool {
|
||||
cache: Map<string, Map<ComponentVariant, any>> = new Map();
|
||||
|
||||
getOrInsert(identifier: IdentifierValue, 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;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const t of this.cache.values()) {
|
||||
for (const i of t.values()) {
|
||||
if (typeof i === 'object' && typeof i[Symbol.dispose] === 'function') {
|
||||
try {
|
||||
i[Symbol.dispose]();
|
||||
} catch (err) {
|
||||
setImmediate(() => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class Resolver extends FrameworkProvider {
|
||||
constructor(
|
||||
public readonly provider: BasicFrameworkProvider,
|
||||
public readonly depth = 0,
|
||||
public readonly stack: IdentifierValue[] = []
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
scope = this.provider.scope;
|
||||
collection = this.provider.collection;
|
||||
eventBus = this.provider.eventBus;
|
||||
|
||||
getRaw(
|
||||
identifier: IdentifierValue,
|
||||
{
|
||||
sameScope = false,
|
||||
optional = false,
|
||||
noCache = false,
|
||||
props,
|
||||
}: ResolveOptions = {}
|
||||
) {
|
||||
const factory = this.provider.collection.getFactory(
|
||||
identifier,
|
||||
this.provider.scope
|
||||
);
|
||||
if (!factory) {
|
||||
if (this.provider.parent && !sameScope) {
|
||||
return this.provider.parent.getRaw(identifier, {
|
||||
sameScope: sameScope,
|
||||
optional,
|
||||
noCache,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
if (optional) {
|
||||
return undefined;
|
||||
}
|
||||
throw new ComponentNotFoundError(identifier);
|
||||
}
|
||||
|
||||
const runFactory = () => {
|
||||
const nextResolver = this.track(identifier);
|
||||
try {
|
||||
return withContext(() => factory(nextResolver), {
|
||||
provider: this.provider,
|
||||
props,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ComponentNotFoundError) {
|
||||
throw new MissingDependencyError(
|
||||
identifier,
|
||||
err.identifier,
|
||||
this.stack
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
if (noCache) {
|
||||
return runFactory();
|
||||
}
|
||||
|
||||
return this.provider.cache.getOrInsert(identifier, runFactory);
|
||||
}
|
||||
|
||||
getAllRaw(
|
||||
identifier: IdentifierValue,
|
||||
{ sameScope = false, noCache, props }: ResolveOptions = {}
|
||||
): Map<ComponentVariant, any> {
|
||||
const vars = this.provider.collection.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<ComponentVariant, any>();
|
||||
|
||||
for (const [variant, factory] of vars) {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
const runFactory = () => {
|
||||
const nextResolver = this.track(identifier);
|
||||
try {
|
||||
return withContext(() => factory(nextResolver), {
|
||||
provider: this.provider,
|
||||
props,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ComponentNotFoundError) {
|
||||
throw new MissingDependencyError(
|
||||
identifier,
|
||||
err.identifier,
|
||||
this.stack
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
let service;
|
||||
if (noCache) {
|
||||
service = runFactory();
|
||||
} else {
|
||||
service = this.provider.cache.getOrInsert(
|
||||
{
|
||||
identifierName: identifier.identifierName,
|
||||
variant,
|
||||
},
|
||||
runFactory
|
||||
);
|
||||
}
|
||||
result.set(variant, service);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
track(identifier: IdentifierValue): Resolver {
|
||||
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 Resolver(this.provider, depth, [...this.stack, identifier]);
|
||||
}
|
||||
|
||||
override dispose(): void {}
|
||||
}
|
||||
|
||||
export class BasicFrameworkProvider extends FrameworkProvider {
|
||||
public readonly cache = new ComponentCachePool();
|
||||
public readonly collection: Framework;
|
||||
public readonly eventBus: EventBus;
|
||||
|
||||
disposed = false;
|
||||
|
||||
constructor(
|
||||
collection: Framework,
|
||||
public readonly scope: string[],
|
||||
public readonly parent: FrameworkProvider | null
|
||||
) {
|
||||
super();
|
||||
this.collection = collection;
|
||||
this.eventBus = new EventBus(this, this.parent?.eventBus);
|
||||
}
|
||||
|
||||
getRaw(identifier: IdentifierValue, options?: ResolveOptions) {
|
||||
const resolver = new Resolver(this);
|
||||
return resolver.getRaw(identifier, options);
|
||||
}
|
||||
|
||||
getAllRaw(
|
||||
identifier: IdentifierValue,
|
||||
options?: ResolveOptions
|
||||
): Map<ComponentVariant, any> {
|
||||
const resolver = new Resolver(this);
|
||||
return resolver.getAllRaw(identifier, options);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
this.disposed = true;
|
||||
this.cache.dispose();
|
||||
}
|
||||
}
|
||||
5
packages/common/infra/src/framework/core/scope.ts
Normal file
5
packages/common/infra/src/framework/core/scope.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { FrameworkScopeStack } from './types';
|
||||
|
||||
export function stringifyScope(scope: FrameworkScopeStack): string {
|
||||
return scope.join('/');
|
||||
}
|
||||
36
packages/common/infra/src/framework/core/types.ts
Normal file
36
packages/common/infra/src/framework/core/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { FrameworkProvider } from './provider';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type Type<T = any> = abstract new (...args: any) => T;
|
||||
|
||||
export type ComponentFactory<T = any> = (provider: FrameworkProvider) => T;
|
||||
export type ComponentVariant = string;
|
||||
|
||||
export type FrameworkScopeStack = string[];
|
||||
|
||||
export type IdentifierValue = {
|
||||
identifierName: string;
|
||||
variant: ComponentVariant;
|
||||
};
|
||||
|
||||
export type GeneralIdentifier<T = any> = Identifier<T> | Type<T>;
|
||||
|
||||
export type Identifier<T> = {
|
||||
identifierName: string;
|
||||
variant: ComponentVariant;
|
||||
__TYPE__: T;
|
||||
};
|
||||
|
||||
export type IdentifierType<T> =
|
||||
T extends Identifier<infer R> ? R : T extends Type<infer R> ? R : never;
|
||||
|
||||
export type TypesToDeps<T> = {
|
||||
[index in keyof T]:
|
||||
| GeneralIdentifier<T[index]>
|
||||
| (T[index] extends (infer I)[] ? [GeneralIdentifier<I>] : never);
|
||||
};
|
||||
|
||||
export type SubComponent = {
|
||||
identifier: Identifier<any>;
|
||||
factory: ComponentFactory;
|
||||
};
|
||||
2
packages/common/infra/src/framework/index.ts
Normal file
2
packages/common/infra/src/framework/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './core';
|
||||
export * from './react';
|
||||
126
packages/common/infra/src/framework/react/index.tsx
Normal file
126
packages/common/infra/src/framework/react/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
|
||||
import type { FrameworkProvider, Scope, Service } from '../core';
|
||||
import { ComponentNotFoundError, Framework } from '../core';
|
||||
import { parseIdentifier } from '../core/identifier';
|
||||
import type { GeneralIdentifier, IdentifierType, Type } from '../core/types';
|
||||
|
||||
export const FrameworkStackContext = React.createContext<FrameworkProvider[]>([
|
||||
Framework.EMPTY.provider(),
|
||||
]);
|
||||
|
||||
export function useService<T extends Service>(
|
||||
identifier: GeneralIdentifier<T>
|
||||
): T {
|
||||
const stack = useContext(FrameworkStackContext);
|
||||
|
||||
let service: T | null = null;
|
||||
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
service = stack[i].getOptional(identifier, {
|
||||
sameScope: true,
|
||||
});
|
||||
|
||||
if (service) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!service) {
|
||||
throw new ComponentNotFoundError(parseIdentifier(identifier));
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get services from the current framework stack.
|
||||
*
|
||||
* Automatically converts the service name to camelCase.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { authService, userService } = useServices({ AuthService, UserService });
|
||||
* ```
|
||||
*/
|
||||
export function useServices<
|
||||
const T extends { [key in string]: GeneralIdentifier<Service> },
|
||||
>(
|
||||
identifiers: T
|
||||
): keyof T extends string
|
||||
? { [key in Uncapitalize<keyof T>]: IdentifierType<T[Capitalize<key>]> }
|
||||
: never {
|
||||
const stack = useContext(FrameworkStackContext);
|
||||
|
||||
const services: any = {};
|
||||
|
||||
for (const [key, value] of Object.entries(identifiers)) {
|
||||
let service;
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
service = stack[i].getOptional(value, {
|
||||
sameScope: true,
|
||||
});
|
||||
|
||||
if (service) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!service) {
|
||||
throw new ComponentNotFoundError(parseIdentifier(value));
|
||||
}
|
||||
|
||||
services[key.charAt(0).toLowerCase() + key.slice(1)] = service;
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
export function useServiceOptional<T extends Service>(
|
||||
identifier: Type<T>
|
||||
): T | null {
|
||||
const stack = useContext(FrameworkStackContext);
|
||||
|
||||
let service: T | null = null;
|
||||
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
service = stack[i].getOptional(identifier, {
|
||||
sameScope: true,
|
||||
});
|
||||
|
||||
if (service) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export const FrameworkRoot = ({
|
||||
framework,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ framework: FrameworkProvider }>) => {
|
||||
return (
|
||||
<FrameworkStackContext.Provider value={[framework]}>
|
||||
{children}
|
||||
</FrameworkStackContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const FrameworkScope = ({
|
||||
scope,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ scope?: Scope }>) => {
|
||||
const stack = useContext(FrameworkStackContext);
|
||||
|
||||
const nextStack = useMemo(() => {
|
||||
if (!scope) return stack;
|
||||
return [...stack, scope.framework];
|
||||
}, [stack, scope]);
|
||||
|
||||
return (
|
||||
<FrameworkStackContext.Provider value={nextStack}>
|
||||
{children}
|
||||
</FrameworkStackContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user