Commit bfd03193 authored by nanahira's avatar nanahira

add AppContext

parent c584a517
import { Empty, Prettify } from '../types'; import { AnyClass, Empty } from '../types';
import { import {
AppContext, AppContext,
AppServiceClass, AppServiceClass,
...@@ -7,49 +7,254 @@ import { ...@@ -7,49 +7,254 @@ import {
AppProvideOptions, AppProvideOptions,
AppContextUsed, AppContextUsed,
} from './types'; } from './types';
import {
createAsyncMethod,
isPromiseLike,
trackPromise,
wrapMaybePromise,
} from './promise-utils';
const ServiceClassPrefix = 'class:';
const ProvidePrefix = 'provide:'; const ProvidePrefix = 'provide:';
const getMethodDescriptor = (cls: AnyClass, key: PropertyKey) => {
let proto = cls.prototype;
while (proto && proto !== Object.prototype) {
const desc = Object.getOwnPropertyDescriptor(proto, key as any);
if (desc) return desc;
proto = Object.getPrototypeOf(proto);
}
return undefined;
};
type LoadEntry = {
classRef: AnyClass;
inst: any;
methodKeys: Set<PropertyKey>;
pendingSets: Array<{ key: PropertyKey; value: any }>;
};
type ObjectStep = (ctx: AppContextCore<any, any>) => void;
const flushPendingSets = (entry: LoadEntry) => {
if (isPromiseLike(entry.inst)) return;
if (!entry.pendingSets.length) return;
for (const item of entry.pendingSets) {
(entry.inst as any)[item.key] = item.value;
}
entry.pendingSets.length = 0;
};
const resolveEntryIfNeeded = async (entry: LoadEntry) => {
if (isPromiseLike(entry.inst)) {
entry.inst = await entry.inst;
}
flushPendingSets(entry);
return entry.inst;
};
export class AppContextCore<Cur = Empty, Req = Empty> { export class AppContextCore<Cur = Empty, Req = Empty> {
private __current: Cur; private __current: Cur;
private __required: Req; private __required: Req;
registry = new Map<string, any>(); registry = new Map<string | AnyClass, LoadEntry>();
loadSeq: any[] = []; loadSeq: LoadEntry[] = [];
objectSteps: ObjectStep[] = [];
provide< provide<
C extends AppServiceClass<Cur>, C extends AppServiceClass<Cur, Req>,
const P extends string = '', const P extends string = '',
const M extends (keyof InstanceType<C>)[] = [], const M extends (keyof InstanceType<C>)[] = [],
>( >(
cls: C, cls: C,
...args: AppProvideArgs<Cur, Req, C, P, M> ...args: AppProvideArgs<Cur, Req, C, P, M>
): AppProvidedMerged<Cur, Req, C, P, M> { ): AppProvidedMerged<Cur, Req, C, P, M> {
const options = args[args.length - 1] as const last = args[args.length - 1] as any;
const hasOptions =
!!last &&
typeof last === 'object' &&
('provide' in last ||
'merge' in last ||
'useValue' in last ||
'useFactory' in last ||
'useClass' in last);
const options = (hasOptions ? last : undefined) as
| AppProvideOptions<Cur, Req, C, P, M> | AppProvideOptions<Cur, Req, C, P, M>
| undefined; | undefined;
const _args: ConstructorParameters<C> = args; const _args = (
hasOptions ? args.slice(0, -1) : args
) as ConstructorParameters<C>;
const inst = const inst =
options?.useValue ?? options?.useValue ??
(options?.useFactory (options?.useFactory
? options.useFactory(this as any, ..._args) ? options.useFactory(this as any, ..._args)
: new (options?.useClass ?? cls)(this as any, _args)); : new (options?.useClass ?? cls)(this as any, ..._args));
this.registry.set(ServiceClassPrefix + cls.name, inst); const classRef = cls as unknown as AnyClass;
const provideKey = options?.provide
? ProvidePrefix + String(options.provide)
: undefined;
const methodKeys = new Set<PropertyKey>();
for (const name of Object.getOwnPropertyNames(cls.prototype)) {
if (name === 'constructor') continue;
const desc = Object.getOwnPropertyDescriptor(cls.prototype, name);
if (desc && typeof desc.value === 'function') {
methodKeys.add(name);
}
}
const entry: LoadEntry = {
classRef,
inst,
methodKeys,
pendingSets: [],
};
this.registry.set(classRef, entry);
if (provideKey) this.registry.set(provideKey, entry);
this.loadSeq.push(entry);
if (options?.provide) {
const prop = options.provide;
const step: ObjectStep = (ctx) => {
Object.defineProperty(ctx, prop, {
enumerable: true,
configurable: true,
get: () => {
const currentEntry = ctx.registry.get(classRef);
return wrapMaybePromise(currentEntry?.inst, { methodKeys });
},
});
};
step(this);
this.objectSteps.push(step);
}
if (options?.merge?.length) {
for (const key of options.merge) {
const desc = getMethodDescriptor(cls, key);
const isMethod = !!desc && typeof desc.value === 'function';
const step: ObjectStep = (ctx) => {
if (isMethod) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => {
const currentEntry = ctx.registry.get(classRef);
const currentInst = currentEntry?.inst;
if (isPromiseLike(currentInst))
return createAsyncMethod(currentInst, key);
const fn = (currentInst as any)?.[key];
return typeof fn === 'function' ? fn.bind(currentInst) : fn;
},
});
return;
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => {
const currentEntry = ctx.registry.get(classRef);
const target = wrapMaybePromise(currentEntry?.inst);
return (target as any)?.[key];
},
set: (value: any) => {
const currentEntry = ctx.registry.get(classRef);
if (!currentEntry) return;
if (!isPromiseLike(currentEntry.inst)) {
(currentEntry.inst as any)[key] = value;
return;
}
const state = trackPromise(currentEntry.inst);
if (state.status === 'fulfilled') {
currentEntry.inst = state.value;
flushPendingSets(currentEntry);
(currentEntry.inst as any)[key] = value;
return;
}
if (state.status === 'rejected') throw state.error;
currentEntry.pendingSets.push({ key, value });
},
});
};
step(this);
this.objectSteps.push(step);
}
}
return this as any; return this as any;
} }
get<R>(cls: AppServiceClass<Cur, Req, any, R>): R {} get<R>(cls: AppServiceClass<Cur, Req, any, R>): R {
const key = cls as unknown as AnyClass;
if (!this.registry.has(key)) {
throw new Error(`Service not provided: ${cls.name}`);
}
const entry = this.registry.get(key)!;
const inst = entry.inst;
if (isPromiseLike(inst)) {
const state = trackPromise(inst);
if (state.status === 'fulfilled') {
entry.inst = state.value;
flushPendingSets(entry);
return state.value as any;
}
if (state.status === 'rejected') throw state.error;
return wrapMaybePromise(inst, {
methodKeys: entry.methodKeys,
}) as any;
}
flushPendingSets(entry);
return inst as any;
}
getAsync<R>(cls: AppServiceClass<Cur, Req, any, R>): Promise<R> {} async getAsync<R>(cls: AppServiceClass<Cur, Req, any, R>): Promise<R> {
const key = cls as unknown as AnyClass;
if (!this.registry.has(key)) {
throw new Error(`Service not provided: ${cls.name}`);
}
const entry = this.registry.get(key)!;
const resolved = await resolveEntryIfNeeded(entry);
return resolved as R;
}
use<const Ctxes extends AppContext<any, any>[]>( use<const Ctxes extends AppContext<any, any>[]>(
...ctxes: Ctxes ...ctxes: Ctxes
): AppContextUsed<Cur, Req, Ctxes> { ): AppContextUsed<Cur, Req, Ctxes> {
for (const ctx of ctxes) {
const other = ctx as any as AppContextCore<any, any>;
const entryMap = new Map<LoadEntry, LoadEntry>();
if (Array.isArray(other?.loadSeq)) {
const copiedSeq = other.loadSeq.map((item) => ({
...item,
methodKeys: new Set(item.methodKeys ?? []),
pendingSets: [...(item.pendingSets ?? [])],
}));
other.loadSeq.forEach((item, index) => {
entryMap.set(item, copiedSeq[index]);
});
this.loadSeq.push(...copiedSeq);
}
if (other?.registry instanceof Map) {
for (const [key, value] of other.registry.entries()) {
this.registry.set(key, entryMap.get(value) ?? value);
}
}
if (Array.isArray(other?.objectSteps)) {
this.objectSteps.push(...other.objectSteps);
for (const step of other.objectSteps) {
step(this);
}
}
}
return this as any; return this as any;
} }
...@@ -58,6 +263,17 @@ export class AppContextCore<Cur = Empty, Req = Empty> { ...@@ -58,6 +263,17 @@ export class AppContextCore<Cur = Empty, Req = Empty> {
} }
async start(): Promise<Empty extends Req ? AppContext<Cur, Req> : never> { async start(): Promise<Empty extends Req ? AppContext<Cur, Req> : never> {
for (const entry of this.loadSeq) {
await resolveEntryIfNeeded(entry);
}
for (const entry of this.loadSeq) {
const inst = entry.inst;
if (inst && typeof inst.init === 'function') {
await inst.init();
}
}
return this as any; return this as any;
} }
} }
......
export * from './types'; export type * from './types';
export * from './app-context'; export * from './app-context';
export * from './app-service-base'; export * from './app-service-base';
...@@ -17,7 +17,7 @@ export type AppServiceClass< ...@@ -17,7 +17,7 @@ export type AppServiceClass<
Req = Empty, Req = Empty,
A extends any[] = any[], A extends any[] = any[],
R = any, R = any,
> = new (ctx: AppContext<Cur, Req>, ...args: A) => R; > = new (ctx: AppContext<CurAndReq<Cur, Req>>, ...args: A) => R;
export type AppServiceConfig<C extends AppServiceClass> = C extends new ( export type AppServiceConfig<C extends AppServiceClass> = C extends new (
first: any, first: any,
...@@ -29,7 +29,7 @@ export type AppServiceConfig<C extends AppServiceClass> = C extends new ( ...@@ -29,7 +29,7 @@ export type AppServiceConfig<C extends AppServiceClass> = C extends new (
export type AppProvideOptions< export type AppProvideOptions<
Cur, Cur,
Req, Req,
C extends AppServiceClass<Cur>, C extends AppServiceClass<Cur, Req>,
P extends string, P extends string,
M extends (keyof InstanceType<C>)[], M extends (keyof InstanceType<C>)[],
> = { > = {
...@@ -46,7 +46,7 @@ export type AppProvideOptions< ...@@ -46,7 +46,7 @@ export type AppProvideOptions<
export type AppProvideArgs< export type AppProvideArgs<
Cur, Cur,
Req, Req,
C extends AppServiceClass<Cur>, C extends AppServiceClass<Cur, Req>,
P extends string, P extends string,
M extends (keyof InstanceType<C>)[], M extends (keyof InstanceType<C>)[],
> = [ > = [
......
// dual-unified.ts // dual-unified.ts
export type Dual<T> = T & PromiseLike<T>; export type Dual<T> = T & PromiseLike<T>;
export const DUAL_PENDING = Symbol('dual.pending');
export const throwDualPending = (): never => {
throw DUAL_PENDING;
};
type ThenKey = 'then' | 'catch' | 'finally'; type ThenKey = 'then' | 'catch' | 'finally';
const isThenKey = (k: PropertyKey): k is ThenKey => const isThenKey = (k: PropertyKey): k is ThenKey =>
...@@ -7,14 +11,13 @@ const isThenKey = (k: PropertyKey): k is ThenKey => ...@@ -7,14 +11,13 @@ const isThenKey = (k: PropertyKey): k is ThenKey =>
type State = 'undecided' | 'pending' | 'fulfilled' | 'rejected'; type State = 'undecided' | 'pending' | 'fulfilled' | 'rejected';
/** 仅允许填入 “返回 Promise 的方法名” */
export type AsyncMethodKeys<T> = { export type AsyncMethodKeys<T> = {
[K in keyof T]-?: T[K] extends (...args: any[]) => Promise<any> ? K : never; [K in keyof T]-?: T[K] extends (...args: any[]) => Promise<any> ? K : never;
}[keyof T]; }[keyof T];
export interface DualizeOptions<T> { export interface DualizeOptions<T> {
/** 这些方法在 undecided/pending 时会返回一个延迟执行函数,等待对象 Promise 完成后再调用 */ /** 这些方法在 undecided/pending 时会返回延迟执行函数,等待对象 Promise 完成后再调用 */
asyncMethods?: readonly AsyncMethodKeys<T>[]; asyncMethods?: readonly (keyof T)[];
} }
export function dualizeAny<T>( export function dualizeAny<T>(
...@@ -59,6 +62,10 @@ export function dualizeAny<T>( ...@@ -59,6 +62,10 @@ export function dualizeAny<T>(
value = sync(); value = sync();
state = 'fulfilled'; state = 'fulfilled';
} catch (e) { } catch (e) {
if (e === DUAL_PENDING) {
startAsync();
return;
}
reason = e; reason = e;
state = 'rejected'; state = 'rejected';
} }
...@@ -139,6 +146,9 @@ export function dualizeAny<T>( ...@@ -139,6 +146,9 @@ export function dualizeAny<T>(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
if (state === 'fulfilled') return getFrom(value, prop); if (state === 'fulfilled') return getFrom(value, prop);
if ((state as State) === 'pending') {
throw new TypeError('Value is not ready yet. Please await it first.');
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
if (state === 'rejected') throw reason; if (state === 'rejected') throw reason;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment