Commit e6abd888 authored by nanahira's avatar nanahira

support fork plugin

parent f0b6284e
import { Context } from 'koishi'; import { Context } from 'koishi';
import { ClassType } from 'schemastery-gen'; import { ClassType } from 'schemastery-gen';
import { InjectConfig, PluginSchema } from './decorators'; import { Fork, InjectConfig, PluginSchema } from './decorators';
import { ExactClassPluginConfig, PluginClass, TypeFromClass } from './def';
import { LifecycleEvents } from './register';
export type PartialDeep<T> = T extends export type PartialDeep<T> = T extends
| string | string
...@@ -45,3 +47,46 @@ export function StarterPlugin<C>(config: ClassType<C>) { ...@@ -45,3 +47,46 @@ export function StarterPlugin<C>(config: ClassType<C>) {
PluginSchema(config)(plugin); PluginSchema(config)(plugin);
return plugin; return plugin;
} }
export function ParentPlugin<PC extends PluginClass>(child: PC) {
const plugin = class ParentPluginBase
extends StarterPlugin<ExactClassPluginConfig<PC>>(child['Config'])
implements LifecycleEvents
{
onFork(instance: TypeFromClass<PC>): void | Promise<void> {}
onForkDisconnect(instance: TypeFromClass<PC>): void | Promise<void> {}
};
Fork(child)(plugin);
return plugin;
}
export function ParentPluginSet<PC extends PluginClass>(child: PC) {
return class ParentPluginSet extends ParentPlugin(child) {
instances = new Set<TypeFromClass<PC>>();
onFork(instance: TypeFromClass<PC>) {
this.instances.add(instance);
}
onForkDisconnect(instance: TypeFromClass<PC>) {
this.instances.delete(instance);
}
};
}
export function ParentPluginMap<PC extends PluginClass, R>(
child: PC,
classifyFunction: (instance: TypeFromClass<PC>) => R,
) {
return class ParentPluginMapBase extends ParentPlugin(child) {
instances = new Map<R, TypeFromClass<PC>>();
onFork(instance: TypeFromClass<PC>) {
this.instances.set(classifyFunction(instance), instance);
}
onForkDisconnect(instance: TypeFromClass<PC>) {
this.instances.delete(classifyFunction(instance));
}
};
}
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
KoishiServiceProvideSym, KoishiServiceProvideSym,
KoishiSystemInjectSym, KoishiSystemInjectSym,
KoishiSystemInjectSymKeys, KoishiSystemInjectSymKeys,
PluginClass,
ProvideOptions, ProvideOptions,
ServiceName, ServiceName,
SystemInjectFun, SystemInjectFun,
...@@ -98,6 +99,7 @@ export const InjectApp = () => InjectSystem((obj) => obj.__ctx.app); ...@@ -98,6 +99,7 @@ export const InjectApp = () => InjectSystem((obj) => obj.__ctx.app);
export const InjectConfig = () => InjectSystem((obj) => obj.__config); export const InjectConfig = () => InjectSystem((obj) => obj.__config);
export const InjectLogger = (name?: string) => export const InjectLogger = (name?: string) =>
InjectSystem((obj) => obj.__ctx.logger(name || obj.constructor.name)); InjectSystem((obj) => obj.__ctx.logger(name || obj.constructor.name));
export const InjectParent = () => InjectSystem((obj) => obj.__ctx.__parent);
export const Caller = () => export const Caller = () =>
InjectSystem((obj) => { InjectSystem((obj) => {
const targetCtx: Context = obj[Context.current] || obj.__ctx; const targetCtx: Context = obj[Context.current] || obj.__ctx;
...@@ -151,3 +153,6 @@ export const MixinModel = <K extends Keys<Tables>>( ...@@ -151,3 +153,6 @@ export const MixinModel = <K extends Keys<Tables>>(
const registrar = new ModelRegistrar(ctx.model); const registrar = new ModelRegistrar(ctx.model);
registrar.mixinModel(tableName, classDict); registrar.mixinModel(tableName, classDict);
}); });
export const Fork = (forkPlugin: PluginClass) =>
Metadata.set('KoishiFork', forkPlugin);
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { Context, Schema } from 'koishi'; import { Context, Schema } from 'koishi';
import { import {
ControlType, ControlType,
PluginClass,
ProvideDefinition, ProvideDefinition,
ServiceName, ServiceName,
SystemInjectFun, SystemInjectFun,
...@@ -32,6 +33,7 @@ export interface MetadataMap { ...@@ -32,6 +33,7 @@ export interface MetadataMap {
KoishiSystemInjectSym: SystemInjectFun; KoishiSystemInjectSym: SystemInjectFun;
KoishiPredefineSchema: Schema | ClassType<any>; KoishiPredefineSchema: Schema | ClassType<any>;
KoishiPredefineName: string; KoishiPredefineName: string;
KoishiFork: PluginClass;
} }
export const ThirdEyeSym = Symbol('ThirdEyeSym'); export const ThirdEyeSym = Symbol('ThirdEyeSym');
...@@ -7,7 +7,7 @@ export * from 'koishi-decorators/dist/src/def/interfaces'; ...@@ -7,7 +7,7 @@ export * from 'koishi-decorators/dist/src/def/interfaces';
export type SystemInjectFun = <T = any>(obj: PluginMeta<T>) => any; export type SystemInjectFun = <T = any>(obj: PluginMeta<T>) => any;
export type ServiceName = keyof Context; export type ServiceName = keyof Context | string;
export interface ProvideOptions extends Context.ServiceOptions { export interface ProvideOptions extends Context.ServiceOptions {
immediate?: boolean; immediate?: boolean;
...@@ -42,6 +42,9 @@ export type ClassPluginConfig<P extends PluginClass> = P extends PluginClass< ...@@ -42,6 +42,9 @@ export type ClassPluginConfig<P extends PluginClass> = P extends PluginClass<
? C ? C
: never; : never;
export type ExactClassPluginConfig<P extends PluginClass> =
P extends PluginClass<any, { config: infer IC }> ? IC : ClassPluginConfig<P>;
export type MapPluginToConfig<M extends Dict<PluginClass>> = { export type MapPluginToConfig<M extends Dict<PluginClass>> = {
[K in keyof M]: ClassPluginConfig<M[K]>; [K in keyof M]: ClassPluginConfig<M[K]>;
}; };
...@@ -61,3 +64,5 @@ export interface ControlType< ...@@ -61,3 +64,5 @@ export interface ControlType<
type: T; type: T;
condition: Condition<ControlTypeMap[T], any, [Record<string, any>]>; condition: Condition<ControlTypeMap[T], any, [Record<string, any>]>;
} }
export type Prop<T> = T;
...@@ -47,6 +47,7 @@ export class MultiInstancePluginFramework<InnerPlugin extends PluginClass> ...@@ -47,6 +47,7 @@ export class MultiInstancePluginFramework<InnerPlugin extends PluginClass>
delete this.instances; delete this.instances;
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function
onApply() {} onApply() {}
} }
......
...@@ -30,7 +30,7 @@ export interface PluginMeta<T = any> { ...@@ -30,7 +30,7 @@ export interface PluginMeta<T = any> {
__config: T; __config: T;
__registrar: Registrar; __registrar: Registrar;
__pluginOptions: KoishiPluginRegistrationOptions<T>; __pluginOptions: KoishiPluginRegistrationOptions<T>;
__forkInstances: any[]; __promisesToWaitFor: Promise<void>[];
} }
export interface OnApply { export interface OnApply {
...@@ -49,6 +49,14 @@ export interface LifecycleEvents { ...@@ -49,6 +49,14 @@ export interface LifecycleEvents {
onApply?(): void; onApply?(): void;
onConnect?(): void | Promise<void>; onConnect?(): void | Promise<void>;
onDisconnect?(): void | Promise<void>; onDisconnect?(): void | Promise<void>;
onFork?(instance: any): void | Promise<void>;
onForkDisconnect?(instance: any): void | Promise<void>;
}
declare module 'koishi' {
interface Context {
__parent?: any;
}
} }
export function DefinePlugin<T>( export function DefinePlugin<T>(
...@@ -78,24 +86,25 @@ export function DefinePlugin<T>( ...@@ -78,24 +86,25 @@ export function DefinePlugin<T>(
} }
const newClass = class extends originalClass implements PluginMeta { const newClass = class extends originalClass implements PluginMeta {
static get Config() { static get Config() {
const schemaType = const schemaType = reflector.get('KoishiPredefineSchema', newClass);
reflector.get('KoishiPredefineSchema', newClass) ||
reflector.get('KoishiPredefineSchema', originalClass);
return schemaType ? SchemaClass(schemaType) : undefined; return schemaType ? SchemaClass(schemaType) : undefined;
} }
static get using() { static get using() {
const list = reflector const list = reflector.getArray(KoishiAddUsingList, newClass);
.getArray(KoishiAddUsingList, originalClass)
.concat(reflector.getArray(KoishiAddUsingList, newClass));
return _.uniq(list); return _.uniq(list);
} }
static get reusable() {
const fork = reflector.get('KoishiFork', newClass);
return !!fork;
}
__ctx: Context; __ctx: Context;
__config: T; __config: T;
__pluginOptions: KoishiPluginRegistrationOptions<T>; __pluginOptions: KoishiPluginRegistrationOptions<T>;
__registrar: Registrar; __registrar: Registrar;
__promisesToWaitFor: Promise<void>[]; __promisesToWaitFor: Promise<void>[];
__forkInstances: any[];
_handleSystemInjections() { _handleSystemInjections() {
const injectKeys = reflector.getArray(KoishiSystemInjectSymKeys, this); const injectKeys = reflector.getArray(KoishiSystemInjectSymKeys, this);
...@@ -228,10 +237,7 @@ export function DefinePlugin<T>( ...@@ -228,10 +237,7 @@ export function DefinePlugin<T>(
} }
_getProvidingServices() { _getProvidingServices() {
return [ return reflector.getArray(KoishiServiceProvideSym, this);
...reflector.getArray(KoishiServiceProvideSym, originalClass),
...reflector.getArray(KoishiServiceProvideSym, this),
];
} }
_handleServiceProvide(immediate: boolean) { _handleServiceProvide(immediate: boolean) {
...@@ -275,6 +281,29 @@ export function DefinePlugin<T>( ...@@ -275,6 +281,29 @@ export function DefinePlugin<T>(
}); });
} }
_initializeFork() {
let fork = reflector.get('KoishiFork', this);
if (!fork) {
return;
}
if (!fork[ThirdEyeSym]) {
fork = DefinePlugin()(fork);
}
this.__ctx.on('fork', (ctx, options) => {
ctx.__parent = this;
const instance = new fork(ctx, options);
ctx.on('dispose', () => {
if (typeof this.onForkDisconnect === 'function') {
this.onForkDisconnect(instance);
}
delete ctx.__parent;
});
if (typeof this.onFork === 'function') {
this.onFork(instance);
}
});
}
_initializePluginClass() { _initializePluginClass() {
this._handleSystemInjections(); this._handleSystemInjections();
this._handleServiceInjections(); this._handleServiceInjections();
...@@ -284,20 +313,19 @@ export function DefinePlugin<T>( ...@@ -284,20 +313,19 @@ export function DefinePlugin<T>(
this.onApply(); this.onApply();
} }
this._handleServiceProvide(true); this._handleServiceProvide(true);
this._initializeFork();
this._registerAfterInit(); this._registerAfterInit();
} }
constructor(...args: any[]) { constructor(...args: any[]) {
const originalCtx: Context = args[0]; const originalCtx: Context = args[0];
const config = args[1]; const config = args[1];
const ctx = new Registrar(originalClass, newClass).getScopeContext( const ctx = new Registrar(newClass).getScopeContext(originalCtx);
originalCtx,
);
super(ctx, config, ...args.slice(2)); super(ctx, config, ...args.slice(2));
this.__ctx = ctx; this.__ctx = ctx;
this.__config = config; this.__config = config;
this.__pluginOptions = options; this.__pluginOptions = options;
this.__registrar = new Registrar(this, originalClass, config); this.__registrar = new Registrar(this, undefined, config);
this.__promisesToWaitFor = []; this.__promisesToWaitFor = [];
this._initializePluginClass(); this._initializePluginClass();
} }
...@@ -306,9 +334,7 @@ export function DefinePlugin<T>( ...@@ -306,9 +334,7 @@ export function DefinePlugin<T>(
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get: () => get: () =>
reflector.get('KoishiPredefineName', newClass) || reflector.get('KoishiPredefineName', newClass) || originalClass.name,
reflector.get('KoishiPredefineName', originalClass) ||
originalClass.name,
}); });
newClass[ThirdEyeSym] = true; newClass[ThirdEyeSym] = true;
return newClass; return newClass;
......
import { RegisterSchema, SchemaProperty } from 'schemastery-gen';
import { DefinePlugin } from '../src/register';
import { ParentPluginMap, StarterPlugin } from '../src/base-plugin';
import { Fork, InjectParent, Provide } from '../src/decorators';
import { UseCommand } from 'koishi-decorators';
import { App } from 'koishi';
import { Prop } from '../src/def';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
forkTest: MyPlugin;
}
}
}
@RegisterSchema()
class Config {
@SchemaProperty()
name: string;
getName() {
return this.name;
}
}
@DefinePlugin()
class ChildPlugin extends StarterPlugin(Config) {
@InjectParent()
parent: Prop<MyPlugin>;
@UseCommand('parent{{name}}')
async onParentCommand() {
return this.parent.config.getName();
}
@UseCommand('child{{name}}')
async onSelfCommand() {
return this.config.getName();
}
}
@Provide('forkTest', { immediate: true })
@DefinePlugin()
class MyPlugin extends ParentPluginMap(ChildPlugin, (p) => p.config.getName()) {
isParent = true;
}
describe('Fork', () => {
let app: App;
beforeEach(async () => {
app = new App();
await app.start();
});
it('should fork a plugin', async () => {
app.plugin(MyPlugin, { name: 'a' });
const myPlugin = app.forkTest;
expect(app.forkTest.config.getName()).toEqual('a');
expect(app.forkTest.instances.get('a').config.getName()).toEqual('a');
expect(app.forkTest.instances.get('a').parent).toEqual(myPlugin);
expect(app.forkTest.instances.get('b')).toBeUndefined();
app.plugin(MyPlugin, { name: 'b' });
expect(app.forkTest.instances.get('b').config.getName()).toEqual('b');
expect(myPlugin.instances.get('b').parent).toEqual(app.forkTest);
const commandChildA = app.command('childa');
const commandChildB = app.command('childb');
const commandParentA = app.command('parenta');
const commandParentB = app.command('parentb');
expect(await commandChildA.execute({})).toEqual('a');
expect(await commandChildB.execute({})).toEqual('b');
expect(await commandParentA.execute({})).toEqual('a');
// expect(await commandParentB.execute({})).toEqual('a');
});
});
import { RegisterSchema, SchemaProperty, StarterPlugin } from '..'; import { RegisterSchema, SchemaProperty, StarterPlugin } from '..';
import { Assets, Bot, Cache, Context } from 'koishi'; import { Assets, Bot, Cache } from 'koishi';
import { Inject, PluginName, UsingService } from '../src/decorators'; import { Inject, PluginName, UsingService } from '../src/decorators';
import { DefinePlugin } from '../src/register'; import { DefinePlugin } from '../src/register';
import { ServiceName } from '../src/def'; import { ServiceName } from '../src/def';
......
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