From 90244321403847c450184325e3b632c2b94f1870 Mon Sep 17 00:00:00 2001 From: Shigma Date: Thu, 16 Jun 2022 06:44:47 +0800 Subject: [PATCH] refa: reorganize class files --- src/app.ts | 2 +- src/context.ts | 6 +- src/index.ts | 2 +- src/lifecycle.ts | 7 +- src/plugin.ts | 232 ++++++++++++++--------------------------------- src/registry.ts | 93 ------------------- src/state.ts | 187 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 265 insertions(+), 264 deletions(-) delete mode 100644 src/registry.ts create mode 100644 src/state.ts diff --git a/src/app.ts b/src/app.ts index c3b3e79..a96a5d7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,6 @@ import { Context } from './context' import { Lifecycle } from './lifecycle' -import { Registry } from './registry' +import { Registry } from './plugin' export class App extends Context { options: App.Config diff --git a/src/context.ts b/src/context.ts index 72fb446..5e42b64 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,8 +1,8 @@ import { defineProperty } from 'cosmokit' import { App } from './app' import { Lifecycle } from './lifecycle' -import { Plugin } from './plugin' -import { Registry } from './registry' +import { State } from './state' +import { Registry } from './plugin' export type Filter = (session: Lifecycle.Session) => boolean @@ -90,7 +90,7 @@ export namespace Context { export interface Meta { app: App - state: Plugin.State + state: State filter: Filter mapping: {} } diff --git a/src/index.ts b/src/index.ts index 1cddb00..8e53a3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,5 @@ export * from './app' export * from './context' export * from './lifecycle' export * from './plugin' -export * from './registry' export * from './service' +export * from './state' diff --git a/src/lifecycle.ts b/src/lifecycle.ts index 0cc4b6e..8efec7f 100644 --- a/src/lifecycle.ts +++ b/src/lifecycle.ts @@ -1,5 +1,6 @@ import { Awaitable, defineProperty, Promisify, remove } from 'cosmokit' import { Context } from './context' +import { Fork, Runtime } from './state' import { Plugin } from './plugin' function isBailed(value: any) { @@ -224,13 +225,13 @@ type BeforeEventName = OmitSubstring export type BeforeEventMap = { [E in EventName & string as OmitSubstring]: Events[E] } export interface Events { - 'plugin-added'(state: Plugin.Runtime): void - 'plugin-removed'(state: Plugin.Runtime): void + 'plugin-added'(state: Runtime): void + 'plugin-removed'(state: Runtime): void 'ready'(): Awaitable 'fork': Plugin.Function 'dispose'(): Awaitable 'internal/warn'(format: any, ...param: any[]): void 'internal/service'(this: Context, name: string, oldValue: any): void - 'internal/update'(state: Plugin.Fork, config: any): void + 'internal/update'(state: Fork, config: any): void 'internal/hook'(name: string, listener: Function, prepend: boolean): () => boolean } diff --git a/src/plugin.ts b/src/plugin.ts index ab75042..545ba47 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,16 +1,10 @@ -import { defineProperty, remove } from 'cosmokit' +import { App } from './app' import { Context } from './context' -import { Registry } from './registry' - -function isConstructor(func: Function) { - // async function or arrow function - if (!func.prototype) return false - // generator function or malformed definition - if (func.prototype.constructor !== func) return false - return true -} +import { Fork, Runtime } from './state' -export type Disposable = () => void +function isApplicable(object: Plugin) { + return object && typeof object === 'object' && typeof object.apply === 'function' +} export type Plugin = Plugin.Function | Plugin.Object @@ -32,178 +26,90 @@ export namespace Plugin { : T extends Function ? U : T extends Object ? U : never +} - export abstract class State { - uid: number - runtime: Runtime - context: Context - disposables: Disposable[] = [] - - abstract dispose(): boolean - abstract restart(): void - abstract update(config: any, manual?: boolean): void - - constructor(public parent: Context, public config: any) { - this.uid = parent.app.counter++ - this.context = parent.extend({ state: this }) - } +export namespace Registry { + export interface Config {} - protected clear(preserve = false) { - this.disposables = this.disposables.splice(0, Infinity).filter((dispose) => { - if (preserve && dispose[kPreserve]) return true - dispose() - }) - } + export interface Delegates { + using(using: readonly string[], callback: Plugin.Function): Fork + plugin(plugin: T, config?: boolean | Plugin.Config): Fork + dispose(plugin?: Plugin): Runtime } +} - export const kPreserve = Symbol('preserve') - - export class Fork extends State { - constructor(parent: Context, config: any, runtime: Runtime) { - super(parent, config) - this.runtime = runtime - this.dispose = this.dispose.bind(this) - defineProperty(this.dispose, kPreserve, true) - defineProperty(this.dispose, 'name', `state <${parent.source}>`) - runtime.children.push(this) - runtime.disposables.push(this.dispose) - parent.state?.disposables.push(this.dispose) - this.restart() - } - - restart() { - this.clear() - if (!this.runtime.isActive) return - for (const fork of this.runtime.forkables) { - fork(this.context, this.config) - } - } - - update(config: any, manual = false) { - const oldConfig = this.config - const resolved = Registry.validate(this.runtime.plugin, config) - this.config = resolved - if (!manual) { - this.context.emit('internal/update', this, config) - } - if (this.runtime.isForkable) { - this.restart() - } else if (this.runtime.config === oldConfig) { - this.runtime.config = resolved - this.runtime.restart() - } - } +export class Registry extends Map { + constructor(public app: App, private config: Registry.Config) { + super() + app.state = new Runtime(this, null, config) + } - dispose() { - this.clear() - remove(this.runtime.disposables, this.dispose) - if (remove(this.runtime.children, this) && !this.runtime.children.length) { - this.runtime.dispose() - } - return remove(this.parent.state.disposables, this.dispose) - } + get caller(): Context { + return this[Context.current] || this.app } - export class Runtime extends State { - runtime = this - schema: any - using: readonly string[] = [] - forkables: Function[] = [] - children: Fork[] = [] - isActive: boolean - - constructor(private registry: Registry, public plugin: Plugin, config: any) { - super(registry.caller, config) - registry.set(plugin, this) - if (plugin) this.init() - } + private resolve(plugin: Plugin) { + return plugin && (typeof plugin === 'function' ? plugin : plugin.apply) + } - get isForkable() { - return this.forkables.length > 0 - } + get(plugin: Plugin) { + return super.get(this.resolve(plugin)) + } - fork(parent: Context, config: any) { - return new Fork(parent, config, this) - } + has(plugin: Plugin) { + return super.has(this.resolve(plugin)) + } - dispose() { - this.clear() - if (this.plugin) { - const result = this.registry.delete(this.plugin) - this.context.emit('plugin-removed', this) - return result - } - } + set(plugin: Plugin, state: Runtime) { + return super.set(this.resolve(plugin), state) + } - init() { - this.schema = this.plugin['Config'] || this.plugin['schema'] - this.using = this.plugin['using'] || [] - this.context.emit('plugin-added', this) + delete(plugin: Plugin) { + return super.delete(this.resolve(plugin)) + } - if (this.plugin['reusable']) { - this.forkables.push(this.apply) - } + using(using: readonly string[], callback: Plugin.Function) { + return this.plugin({ using, apply: callback, name: callback.name }) + } - if (this.using.length) { - const dispose = this.context.on('internal/service', (name) => { - if (!this.using.includes(name)) return - this.restart() - }) - defineProperty(dispose, kPreserve, true) - } + static validate(plugin: any, config: any) { + if (config === false) return + if (config === true) config = undefined + config ??= {} - this.restart() - } + const schema = plugin['Config'] || plugin['schema'] + if (schema) config = schema(config) + return config + } - private apply = (context: Context, config: any) => { - if (typeof this.plugin !== 'function') { - this.plugin.apply(context, config) - } else if (isConstructor(this.plugin)) { - // eslint-disable-next-line new-cap - const instance = new this.plugin(context, config) - const name = instance[Context.immediate] - if (name) { - context[name] = instance - } - if (instance['fork']) { - this.forkables.push(instance['fork']) - } - } else { - this.plugin(context, config) - } + plugin(plugin: Plugin, config?: any) { + // check if it's a valid plugin + if (typeof plugin !== 'function' && !isApplicable(plugin)) { + throw new Error('invalid plugin, expect function or object with an "apply" method') } - restart() { - this.isActive = false - this.clear(true) - if (this.using.some(name => !this.context[name])) return - - // execute plugin body - this.isActive = true - if (!this.plugin['reusable']) { - this.apply(this.context, this.config) - } + // validate plugin config + config = Registry.validate(plugin, config) + if (!config) return - for (const fork of this.children) { - fork.restart() + // check duplication + const context = this.caller + const duplicate = this.get(plugin) + if (duplicate) { + if (!duplicate.isForkable) { + this.app.emit('internal/warn', `duplicate plugin detected: ${plugin.name}`) } + return duplicate.fork(context, config) } - update(config: any, manual = false) { - if (this.isForkable) { - this.context.emit('internal/warn', `attempting to update forkable plugin "${this.plugin.name}", which may lead unexpected behavior`) - } - const oldConfig = this.config - const resolved = Registry.validate(this.runtime.plugin, config) - this.config = resolved - for (const fork of this.children) { - if (fork.config !== oldConfig) continue - fork.config = resolved - if (!manual) { - this.context.emit('internal/update', fork, config) - } - } - this.restart() - } + const runtime = new Runtime(this, plugin, config) + return runtime.fork(context, config) + } + + dispose(plugin: Plugin) { + const runtime = this.get(plugin) + if (!runtime) return + runtime.dispose() + return runtime } } diff --git a/src/registry.ts b/src/registry.ts deleted file mode 100644 index 0a077c9..0000000 --- a/src/registry.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { App } from './app' -import { Context } from './context' -import { Plugin } from './plugin' - -function isApplicable(object: Plugin) { - return object && typeof object === 'object' && typeof object.apply === 'function' -} - -export namespace Registry { - export interface Config {} - - export interface Delegates { - using(using: readonly string[], callback: Plugin.Function): Plugin.Fork - plugin(plugin: T, config?: boolean | Plugin.Config): Plugin.Fork - dispose(plugin?: Plugin): Plugin.Runtime - } -} - -export class Registry extends Map { - constructor(public app: App, private config: Registry.Config) { - super() - app.state = new Plugin.Runtime(this, null, config) - } - - get caller(): Context { - return this[Context.current] || this.app - } - - private resolve(plugin: Plugin) { - return plugin && (typeof plugin === 'function' ? plugin : plugin.apply) - } - - get(plugin: Plugin) { - return super.get(this.resolve(plugin)) - } - - has(plugin: Plugin) { - return super.has(this.resolve(plugin)) - } - - set(plugin: Plugin, state: Plugin.Runtime) { - return super.set(this.resolve(plugin), state) - } - - delete(plugin: Plugin) { - return super.delete(this.resolve(plugin)) - } - - using(using: readonly string[], callback: Plugin.Function) { - return this.plugin({ using, apply: callback, name: callback.name }) - } - - static validate(plugin: any, config: any) { - if (config === false) return - if (config === true) config = undefined - config ??= {} - - const schema = plugin['Config'] || plugin['schema'] - if (schema) config = schema(config) - return config - } - - plugin(plugin: Plugin, config?: any) { - // check if it's a valid plugin - if (typeof plugin !== 'function' && !isApplicable(plugin)) { - throw new Error('invalid plugin, expect function or object with an "apply" method') - } - - // validate plugin config - config = Registry.validate(plugin, config) - if (!config) return - - // check duplication - const context = this.caller - const duplicate = this.get(plugin) - if (duplicate) { - if (!duplicate.isForkable) { - this.app.emit('internal/warn', `duplicate plugin detected: ${plugin.name}`) - } - return duplicate.fork(context, config) - } - - const runtime = new Plugin.Runtime(this, plugin, config) - return runtime.fork(context, config) - } - - dispose(plugin: Plugin) { - const runtime = this.get(plugin) - if (!runtime) return - runtime.dispose() - return runtime - } -} diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..4ebb5b3 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,187 @@ +import { defineProperty, remove } from 'cosmokit' +import { Context } from './context' +import { Plugin, Registry } from './plugin' + +export type Disposable = () => void + +function isConstructor(func: Function) { + // async function or arrow function + if (!func.prototype) return false + // generator function or malformed definition + if (func.prototype.constructor !== func) return false + return true +} + +export abstract class State { + uid: number + runtime: Runtime + context: Context + disposables: Disposable[] = [] + + abstract dispose(): boolean + abstract restart(): void + abstract update(config: any, manual?: boolean): void + + constructor(public parent: Context, public config: any) { + this.uid = parent.app.counter++ + this.context = parent.extend({ state: this }) + } + + protected clear(preserve = false) { + this.disposables = this.disposables.splice(0, Infinity).filter((dispose) => { + if (preserve && dispose[kPreserve]) return true + dispose() + }) + } +} + +export const kPreserve = Symbol('preserve') + +export class Fork extends State { + constructor(parent: Context, config: any, runtime: Runtime) { + super(parent, config) + this.runtime = runtime + this.dispose = this.dispose.bind(this) + defineProperty(this.dispose, kPreserve, true) + defineProperty(this.dispose, 'name', `state <${parent.source}>`) + runtime.children.push(this) + runtime.disposables.push(this.dispose) + parent.state?.disposables.push(this.dispose) + this.restart() + } + + restart() { + this.clear() + if (!this.runtime.isActive) return + for (const fork of this.runtime.forkables) { + fork(this.context, this.config) + } + } + + update(config: any, manual = false) { + const oldConfig = this.config + const resolved = Registry.validate(this.runtime.plugin, config) + this.config = resolved + if (!manual) { + this.context.emit('internal/update', this, config) + } + if (this.runtime.isForkable) { + this.restart() + } else if (this.runtime.config === oldConfig) { + this.runtime.config = resolved + this.runtime.restart() + } + } + + dispose() { + this.clear() + remove(this.runtime.disposables, this.dispose) + if (remove(this.runtime.children, this) && !this.runtime.children.length) { + this.runtime.dispose() + } + return remove(this.parent.state.disposables, this.dispose) + } +} + +export class Runtime extends State { + runtime = this + schema: any + using: readonly string[] = [] + forkables: Function[] = [] + children: Fork[] = [] + isActive: boolean + + constructor(private registry: Registry, public plugin: Plugin, config: any) { + super(registry.caller, config) + registry.set(plugin, this) + if (plugin) this.init() + } + + get isForkable() { + return this.forkables.length > 0 + } + + fork(parent: Context, config: any) { + return new Fork(parent, config, this) + } + + dispose() { + this.clear() + if (this.plugin) { + const result = this.registry.delete(this.plugin) + this.context.emit('plugin-removed', this) + return result + } + } + + init() { + this.schema = this.plugin['Config'] || this.plugin['schema'] + this.using = this.plugin['using'] || [] + this.context.emit('plugin-added', this) + + if (this.plugin['reusable']) { + this.forkables.push(this.apply) + } + + if (this.using.length) { + const dispose = this.context.on('internal/service', (name) => { + if (!this.using.includes(name)) return + this.restart() + }) + defineProperty(dispose, kPreserve, true) + } + + this.restart() + } + + private apply = (context: Context, config: any) => { + if (typeof this.plugin !== 'function') { + this.plugin.apply(context, config) + } else if (isConstructor(this.plugin)) { + // eslint-disable-next-line new-cap + const instance = new this.plugin(context, config) + const name = instance[Context.immediate] + if (name) { + context[name] = instance + } + if (instance['fork']) { + this.forkables.push(instance['fork']) + } + } else { + this.plugin(context, config) + } + } + + restart() { + this.isActive = false + this.clear(true) + if (this.using.some(name => !this.context[name])) return + + // execute plugin body + this.isActive = true + if (!this.plugin['reusable']) { + this.apply(this.context, this.config) + } + + for (const fork of this.children) { + fork.restart() + } + } + + update(config: any, manual = false) { + if (this.isForkable) { + this.context.emit('internal/warn', `attempting to update forkable plugin "${this.plugin.name}", which may lead unexpected behavior`) + } + const oldConfig = this.config + const resolved = Registry.validate(this.runtime.plugin, config) + this.config = resolved + for (const fork of this.children) { + if (fork.config !== oldConfig) continue + fork.config = resolved + if (!manual) { + this.context.emit('internal/update', fork, config) + } + } + this.restart() + } +}