Commit e6abd888 authored by nanahira's avatar nanahira

support fork plugin

parent f0b6284e
import { Context } from 'koishi';
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
| string
......@@ -45,3 +47,46 @@ export function StarterPlugin<C>(config: ClassType<C>) {
PluginSchema(config)(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 {
KoishiServiceProvideSym,
KoishiSystemInjectSym,
KoishiSystemInjectSymKeys,
PluginClass,
ProvideOptions,
ServiceName,
SystemInjectFun,
......@@ -98,6 +99,7 @@ export const InjectApp = () => InjectSystem((obj) => obj.__ctx.app);
export const InjectConfig = () => InjectSystem((obj) => obj.__config);
export const InjectLogger = (name?: string) =>
InjectSystem((obj) => obj.__ctx.logger(name || obj.constructor.name));
export const InjectParent = () => InjectSystem((obj) => obj.__ctx.__parent);
export const Caller = () =>
InjectSystem((obj) => {
const targetCtx: Context = obj[Context.current] || obj.__ctx;
......@@ -151,3 +153,6 @@ export const MixinModel = <K extends Keys<Tables>>(
const registrar = new ModelRegistrar(ctx.model);
registrar.mixinModel(tableName, classDict);
});
export const Fork = (forkPlugin: PluginClass) =>
Metadata.set('KoishiFork', forkPlugin);
......@@ -2,6 +2,7 @@
import { Context, Schema } from 'koishi';
import {
ControlType,
PluginClass,
ProvideDefinition,
ServiceName,
SystemInjectFun,
......@@ -32,6 +33,7 @@ export interface MetadataMap {
KoishiSystemInjectSym: SystemInjectFun;
KoishiPredefineSchema: Schema | ClassType<any>;
KoishiPredefineName: string;
KoishiFork: PluginClass;
}
export const ThirdEyeSym = Symbol('ThirdEyeSym');
......@@ -7,7 +7,7 @@ export * from 'koishi-decorators/dist/src/def/interfaces';
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 {
immediate?: boolean;
......@@ -42,6 +42,9 @@ export type ClassPluginConfig<P extends PluginClass> = P extends PluginClass<
? C
: never;
export type ExactClassPluginConfig<P extends PluginClass> =
P extends PluginClass<any, { config: infer IC }> ? IC : ClassPluginConfig<P>;
export type MapPluginToConfig<M extends Dict<PluginClass>> = {
[K in keyof M]: ClassPluginConfig<M[K]>;
};
......@@ -61,3 +64,5 @@ export interface ControlType<
type: T;
condition: Condition<ControlTypeMap[T], any, [Record<string, any>]>;
}
export type Prop<T> = T;
......@@ -47,6 +47,7 @@ export class MultiInstancePluginFramework<InnerPlugin extends PluginClass>
delete this.instances;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onApply() {}
}
......
......@@ -30,7 +30,7 @@ export interface PluginMeta<T = any> {
__config: T;
__registrar: Registrar;
__pluginOptions: KoishiPluginRegistrationOptions<T>;
__forkInstances: any[];
__promisesToWaitFor: Promise<void>[];
}
export interface OnApply {
......@@ -49,6 +49,14 @@ export interface LifecycleEvents {
onApply?(): void;
onConnect?(): 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>(
......@@ -78,24 +86,25 @@ export function DefinePlugin<T>(
}
const newClass = class extends originalClass implements PluginMeta {
static get Config() {
const schemaType =
reflector.get('KoishiPredefineSchema', newClass) ||
reflector.get('KoishiPredefineSchema', originalClass);
const schemaType = reflector.get('KoishiPredefineSchema', newClass);
return schemaType ? SchemaClass(schemaType) : undefined;
}
static get using() {
const list = reflector
.getArray(KoishiAddUsingList, originalClass)
.concat(reflector.getArray(KoishiAddUsingList, newClass));
const list = reflector.getArray(KoishiAddUsingList, newClass);
return _.uniq(list);
}
static get reusable() {
const fork = reflector.get('KoishiFork', newClass);
return !!fork;
}
__ctx: Context;
__config: T;
__pluginOptions: KoishiPluginRegistrationOptions<T>;
__registrar: Registrar;
__promisesToWaitFor: Promise<void>[];
__forkInstances: any[];
_handleSystemInjections() {
const injectKeys = reflector.getArray(KoishiSystemInjectSymKeys, this);
......@@ -228,10 +237,7 @@ export function DefinePlugin<T>(
}
_getProvidingServices() {
return [
...reflector.getArray(KoishiServiceProvideSym, originalClass),
...reflector.getArray(KoishiServiceProvideSym, this),
];
return reflector.getArray(KoishiServiceProvideSym, this);
}
_handleServiceProvide(immediate: boolean) {
......@@ -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() {
this._handleSystemInjections();
this._handleServiceInjections();
......@@ -284,20 +313,19 @@ export function DefinePlugin<T>(
this.onApply();
}
this._handleServiceProvide(true);
this._initializeFork();
this._registerAfterInit();
}
constructor(...args: any[]) {
const originalCtx: Context = args[0];
const config = args[1];
const ctx = new Registrar(originalClass, newClass).getScopeContext(
originalCtx,
);
const ctx = new Registrar(newClass).getScopeContext(originalCtx);
super(ctx, config, ...args.slice(2));
this.__ctx = ctx;
this.__config = config;
this.__pluginOptions = options;
this.__registrar = new Registrar(this, originalClass, config);
this.__registrar = new Registrar(this, undefined, config);
this.__promisesToWaitFor = [];
this._initializePluginClass();
}
......@@ -306,9 +334,7 @@ export function DefinePlugin<T>(
enumerable: true,
configurable: true,
get: () =>
reflector.get('KoishiPredefineName', newClass) ||
reflector.get('KoishiPredefineName', originalClass) ||
originalClass.name,
reflector.get('KoishiPredefineName', newClass) || originalClass.name,
});
newClass[ThirdEyeSym] = true;
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 { Assets, Bot, Cache, Context } from 'koishi';
import { Assets, Bot, Cache } from 'koishi';
import { Inject, PluginName, UsingService } from '../src/decorators';
import { DefinePlugin } from '../src/register';
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