Skip to content

Commit

Permalink
refa: reorganize class files
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 15, 2022
1 parent 741448c commit 9024432
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 264 deletions.
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -90,7 +90,7 @@ export namespace Context {

export interface Meta {
app: App
state: Plugin.State
state: State
filter: Filter
mapping: {}
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
7 changes: 4 additions & 3 deletions src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -224,13 +225,13 @@ type BeforeEventName = OmitSubstring<EventName & string, 'before-'>
export type BeforeEventMap = { [E in EventName & string as OmitSubstring<E, 'before-'>]: 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<void>
'fork': Plugin.Function
'dispose'(): Awaitable<void>
'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
}
232 changes: 69 additions & 163 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -32,178 +26,90 @@ export namespace Plugin {
: T extends Function<infer U> ? U
: T extends Object<infer U> ? 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<void>): Fork
plugin<T extends Plugin>(plugin: T, config?: boolean | Plugin.Config<T>): 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<Plugin, Runtime> {
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<void>) {
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
}
}
Loading

0 comments on commit 9024432

Please sign in to comment.