From 63956992afd49156cd7da528133cab1c9cdf520b Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 4 Mar 2021 15:36:52 +0800 Subject: [PATCH] feat(core): side effect detection --- packages/koishi-core/src/app.ts | 2 +- packages/koishi-core/src/context.ts | 66 +++++++++++++++++------------ packages/koishi/src/worker.ts | 9 ++-- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/koishi-core/src/app.ts b/packages/koishi-core/src/app.ts index 668bba3ec1..3abab3e13e 100644 --- a/packages/koishi-core/src/app.ts +++ b/packages/koishi-core/src/app.ts @@ -182,7 +182,7 @@ export class App extends Context { async stop() { this.status = App.Status.closing // `before-disconnect` event is handled by ctx.disposables - await Promise.all(this.disposables.map(dispose => dispose())) + await Promise.all(this.state.disposables.map(dispose => dispose())) this.status = App.Status.closed this.logger('app').debug('stopped') this.emit('disconnect') diff --git a/packages/koishi-core/src/context.ts b/packages/koishi-core/src/context.ts index ca7e5254b4..27e1c48c6d 100644 --- a/packages/koishi-core/src/context.ts +++ b/packages/koishi-core/src/context.ts @@ -22,7 +22,7 @@ export namespace Plugin { export interface Object { name?: string - disposable?: boolean + sideEffect?: boolean apply: Function } @@ -32,6 +32,7 @@ export namespace Plugin { parent: State children: Plugin[] disposables: Disposable[] + sideEffect?: boolean } } @@ -97,10 +98,6 @@ export class Context { this.app._database = database } - protected get disposables() { - return this.app.registry.get(this._plugin).disposables - } - logger(name: string) { return new Logger(name) } @@ -135,15 +132,28 @@ export class Context { return !session || this.filter(session) } + get state() { + return this.app.registry.get(this._plugin) + } + private removeDisposable(listener: Disposable) { - const index = this.disposables.indexOf(listener) + const index = this.state.disposables.indexOf(listener) if (index >= 0) { - this.disposables.splice(index, 1) + this.state.disposables.splice(index, 1) return true } } - plugin(plugin: T, options?: Plugin.Config) { + private declareSideEffect() { + let state = this.state + while (state && !state.sideEffect) { + state.sideEffect = true + state = state.parent + } + } + + plugin(plugin: T, options?: Plugin.Config): this + plugin(plugin: Plugin, options?: any) { if (options === false) return this if (options === true) options = undefined @@ -154,37 +164,38 @@ export class Context { const ctx: this = Object.create(this) defineProperty(ctx, '_plugin', plugin) - const parent = this.app.registry.get(this._plugin) this.app.registry.set(plugin, { - parent, + parent: this.state, children: [], disposables: [], + sideEffect: false, }) if (typeof plugin === 'function') { - (plugin as Plugin.Function)(ctx, options) + plugin(ctx, options) } else if (plugin && typeof plugin === 'object' && typeof plugin.apply === 'function') { - (plugin as Plugin.Object).apply(ctx, options) + if (plugin.sideEffect) ctx.declareSideEffect() + plugin.apply(ctx, options) } else { this.app.registry.delete(plugin) throw new Error('invalid plugin, expect function or object with an "apply" method') } - parent.children.push(plugin) + this.state.children.push(plugin) return this } async dispose(plugin = this._plugin) { - if (!plugin) throw new Error('cannot use ctx.dispose() outside a plugin') - const registry = this.app.registry.get(plugin) - if (!registry) return + const state = this.app.registry.get(plugin) + if (!state) return + if (state.sideEffect) throw new Error('plugins with side effect cannot be disposed') await Promise.all([ - ...registry.children.slice().map(plugin => this.dispose(plugin)), - ...registry.disposables.map(dispose => dispose()), + ...state.children.slice().map(plugin => this.dispose(plugin)), + ...state.disposables.map(dispose => dispose()), ]) this.app.registry.delete(plugin) - const index = registry.parent.children.indexOf(plugin) - if (index >= 0) registry.parent.children.splice(index, 1) + const index = state.parent.children.indexOf(plugin) + if (index >= 0) state.parent.children.splice(index, 1) } async parallel(name: K, ...args: Parameters): Promise>[]> @@ -264,8 +275,11 @@ export class Context { if (name === 'connect' && this.app.status === App.Status.open) { return _listener(), () => false } else if (name === 'before-disconnect') { - this.disposables[method](_listener) + this.state.disposables[method](_listener) return () => this.removeDisposable(_listener) + } else if (name === 'before-connect') { + // before-connect is side effect + this.declareSideEffect() } const hooks = this.app._hooks[name] ||= [] @@ -278,7 +292,7 @@ export class Context { hooks[method]([this, listener]) const dispose = () => this.off(name, listener) - this.disposables.push(dispose) + this.state.disposables.push(dispose) return dispose } @@ -315,7 +329,7 @@ export class Context { callback() }, ms, ...args) const dispose = () => clearTimeout(timer) - this.disposables.push(dispose) + this.state.disposables.push(dispose) return timer } @@ -325,7 +339,7 @@ export class Context { callback() }, ms, ...args) const dispose = () => clearInterval(timer) - this.disposables.push(dispose) + this.state.disposables.push(dispose) return timer } @@ -370,7 +384,7 @@ export class Context { if (desc) parent.description = desc Object.assign(parent.config, config) - this.disposables.push(() => parent.dispose()) + this.state.disposables.push(() => parent.dispose()) return parent } @@ -469,7 +483,7 @@ const register = Router.prototype.register Router.prototype.register = function (this: Router, ...args) { const layer = register.apply(this, args) const context: Context = this['_koishiContext'] - context['disposables'].push(() => { + context.state.disposables.push(() => { const index = this.stack.indexOf(layer) if (index) this.stack.splice(index, 1) }) diff --git a/packages/koishi/src/worker.ts b/packages/koishi/src/worker.ts index f61ff8a1ab..e5534c8a64 100644 --- a/packages/koishi/src/worker.ts +++ b/packages/koishi/src/worker.ts @@ -151,9 +151,7 @@ const pluginEntries: [string, any?][] = Array.isArray(config.plugins) : Object.entries(config.plugins || {}) for (const [name, options] of pluginEntries) { const [path, plugin] = loadEcosystem('plugin', name) - if (plugin.disposable) { - pluginMap.set(require.resolve(path), [name, options]) - } + pluginMap.set(require.resolve(path), [name, options]) app.plugin(plugin, options) } @@ -220,10 +218,13 @@ function createWatcher() { const dependencies = loadDependencies(filename, declined) if (dependencies.has(path)) { dependencies.forEach(dep => accepted.add(dep)) + const plugin = require(filename) + const state = app.registry.get(plugin) + if (state?.sideEffect) continue // dispose installed plugin plugins.push(filename) - app.dispose(require(filename)) + app.dispose(plugin) } }