From 3bf52e03a6c6e6b3273727baf1624922065df3eb Mon Sep 17 00:00:00 2001 From: Shigma Date: Fri, 31 May 2024 15:23:40 +0800 Subject: [PATCH] feat(loader): refa entry options, decouple service logic --- packages/cordis/src/worker/logger.ts | 2 +- packages/loader/src/entry.ts | 236 ++++---------------------- packages/loader/src/file.ts | 12 +- packages/loader/src/group.ts | 18 +- packages/loader/src/inject.ts | 238 +++++++++++++++++++++++++++ packages/loader/src/loader.ts | 58 +++---- packages/loader/src/tree.ts | 10 +- packages/loader/tests/utils.ts | 6 +- 8 files changed, 317 insertions(+), 263 deletions(-) create mode 100644 packages/loader/src/inject.ts diff --git a/packages/cordis/src/worker/logger.ts b/packages/cordis/src/worker/logger.ts index ca161aa..acab9d2 100644 --- a/packages/cordis/src/worker/logger.ts +++ b/packages/cordis/src/worker/logger.ts @@ -34,7 +34,7 @@ export function apply(ctx: Context, config: Config = {}) { new Logger('app').warn(error) }) - ctx.on('loader/entry', (type, entry) => { + ctx.on('loader/entry-fork', (entry, type) => { if (entry.options.group) return ctx.logger('loader').info('%s plugin %c', type, entry.options.name) }) diff --git a/packages/loader/src/entry.ts b/packages/loader/src/entry.ts index 8a74a6a..5a02090 100644 --- a/packages/loader/src/entry.ts +++ b/packages/loader/src/entry.ts @@ -1,30 +1,18 @@ -import { Context, ForkScope, Inject } from '@cordisjs/core' -import { Dict, isNullable } from 'cosmokit' +import { ForkScope } from '@cordisjs/core' +import { isNullable } from 'cosmokit' import { Loader } from './loader.ts' import { EntryGroup } from './group.ts' import { EntryTree } from './tree.ts' -export namespace Entry { - export interface Options { - id: string - name: string - config?: any - group?: boolean | null - disabled?: boolean | null - intercept?: Dict | null - isolate?: Dict | null - inject?: string[] | Inject | null - } +export interface EntryOptions { + id: string + name: string + config?: any + group?: boolean | null + disabled?: boolean | null } -function swap(target: T, source?: T | null) { - for (const key of Reflect.ownKeys(target)) { - Reflect.deleteProperty(target, key) - } - for (const key of Reflect.ownKeys(source || {})) { - Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!) - } -} +export interface EntryUpdateMeta {} function takeEntries(object: {}, keys: string[]) { const result: [string, any][] = [] @@ -43,66 +31,15 @@ function sortKeys(object: T, prepend = ['id', 'name'], append = [' return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2])) } -export abstract class Realm { - protected store: Dict = Object.create(null) - - abstract get suffix(): string - - access(key: string, create = false) { - if (create) { - return this.store[key] ??= Symbol(`${key}${this.suffix}`) - } else { - return this.store[key] ?? Symbol(`${key}${this.suffix}`) - } - } - - delete(key: string) { - delete this.store[key] - } -} - -export class LocalRealm extends Realm { - constructor(private entry: Entry) { - super() - } - - get suffix() { - return '#' + this.entry.options.id - } -} - -export class GlobalRealm extends Realm { - constructor(private loader: Loader, private label: string) { - super() - } - - get suffix() { - return '@' + this.label - } - - gc(key: string) { - // realm garbage collection - for (const entry of this.loader.entries()) { - // has reference to this realm - if (entry.options.isolate?.[key] === this.label) return - } - this.delete(key) - if (!Object.keys(this.store).length) { - delete this.loader.realms[this.suffix] - } - } -} - export class Entry { static readonly key = Symbol.for('cordis.entry') public fork?: ForkScope public suspend = false public parent!: EntryGroup - public options!: Entry.Options + public options!: EntryOptions public subgroup?: EntryGroup public subtree?: EntryTree - public realm = new LocalRealm(this) constructor(public loader: Loader) {} @@ -114,21 +51,6 @@ export class Entry { return id } - get requiredDeps() { - return Array.isArray(this.options.inject) - ? this.options.inject - : this.options.inject?.required ?? [] - } - - get deps() { - return Array.isArray(this.options.inject) - ? this.options.inject - : [ - ...this.options.inject?.required ?? [], - ...this.options.inject?.optional ?? [], - ] - } - get disabled() { // group is always enabled if (this.options.group) return false @@ -142,93 +64,30 @@ export class Entry { _check() { if (this.disabled) return false - for (const name of this.requiredDeps) { - let key: symbol | undefined = this.parent.ctx[Context.isolate][name] - const label = this.options.isolate?.[name] - if (label) key = this.access(name, label) - if (!key || isNullable(this.parent.ctx[key])) return false - } - return true - } - - access(key: string, label: string | true, create: true): symbol - access(key: string, label: string | true, create?: boolean): symbol | undefined - access(key: string, label: string | true, create = false) { - let realm: Realm | undefined - if (label === true) { - realm = this.realm - } else if (create) { - realm = this.loader.realms[label] ??= new GlobalRealm(this.loader, label) - } else { - realm = this.loader.realms[label] - } - return realm?.access(key, create) + return !this.parent.ctx.bail('loader/entry-check', this) } - async checkService(name: string) { - if (!this.requiredDeps.includes(name)) return - const ready = this._check() - if (ready && !this.fork) { - await this.start() - } else if (!ready && this.fork) { - await this.stop() - } + createContext() { + const ctx = this.parent.ctx.extend() + ctx.emit('loader/context-init', this, ctx) + return ctx } - patch(options: Partial = {}) { + patch(options: Partial = {}) { // step 1: prepare isolate map - const ctx = this.fork?.parent ?? this.parent.ctx.extend({ - [Context.intercept]: Object.create(this.parent.ctx[Context.intercept]), - [Context.isolate]: Object.create(this.parent.ctx[Context.isolate]), - }) - const newMap: Dict = Object.create(this.parent.ctx[Context.isolate]) - for (const [key, label] of Object.entries(this.options.isolate ?? {})) { - newMap[key] = this.access(key, label, true) - } + const ctx = this.fork?.parent ?? this.createContext() + const meta = {} as EntryUpdateMeta + ctx.emit(meta, 'loader/before-patch', this, ctx) - // step 2: generate service diff - const diff: [string, symbol, symbol, symbol, symbol][] = [] - const oldMap = ctx[Context.isolate] - for (const key in { ...oldMap, ...newMap, ...this.loader.delims }) { - if (newMap[key] === oldMap[key]) continue - const delim = this.loader.delims[key] ??= Symbol(`delim:${key}`) - ctx[delim] = Symbol(`${key}#${this.id}`) - for (const symbol of [oldMap[key], newMap[key]]) { - const value = symbol && ctx[symbol] - if (!(value instanceof Object)) continue - const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value - if (!source) { - ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`)) - continue - } - diff.push([key, oldMap[key], newMap[key], ctx[delim], source[delim]]) - if (ctx[delim] !== source[delim]) break - } - } - - // step 3: emit internal/before-service - for (const [key, symbol1, symbol2, flag1, flag2] of diff) { - const self = Object.create(ctx) - self[Context.filter] = (target: Context) => { - if (![symbol1, symbol2].includes(target[Context.isolate][key])) return false - return (flag1 === target[this.loader.delims[key]]) !== (flag1 === flag2) - } - ctx.emit(self, 'internal/before-service', key) - } - - // step 4: update - // step 4.1: patch context + // step 1: set prototype for transferred context Object.setPrototypeOf(ctx, this.parent.ctx) - Object.setPrototypeOf(ctx[Context.isolate], this.parent.ctx[Context.isolate]) - Object.setPrototypeOf(ctx[Context.intercept], this.parent.ctx[Context.intercept]) - swap(ctx[Context.isolate], newMap) - swap(ctx[Context.intercept], this.options.intercept) - // step 4.2: update fork (only when options.config is updated) if (this.fork && 'config' in options) { + // step 2: update fork (when options.config is updated) this.suspend = true this.fork.update(this.options.config) } else if (this.subgroup && 'disabled' in options) { + // step 3: check children (when options.disabled is updated) const tree = this.subtree ?? this.parent.tree for (const options of this.subgroup.data) { tree.store[options.id].update({ @@ -237,40 +96,25 @@ export class Entry { } } - // step 4.3: replace service impl - for (const [, symbol1, symbol2, flag1, flag2] of diff) { - if (flag1 === flag2 && ctx[symbol1] && !ctx[symbol2]) { - ctx.root[symbol2] = ctx.root[symbol1] - delete ctx.root[symbol1] - } - } - - // step 5: emit internal/service - for (const [key, symbol1, symbol2, flag1, flag2] of diff) { - const self = Object.create(ctx) - self[Context.filter] = (target: Context) => { - if (![symbol1, symbol2].includes(target[Context.isolate][key])) return false - return (flag1 === target[this.loader.delims[key]]) !== (flag1 === flag2) - } - ctx.emit(self, 'internal/service', key) - } + ctx.emit(meta, 'loader/after-patch', this, ctx) + return ctx + } - // step 6: clean up delimiters - for (const key in this.loader.delims) { - if (!Reflect.ownKeys(newMap).includes(key)) { - delete ctx[this.loader.delims[key]] - } + async refresh() { + const ready = this._check() + if (ready && !this.fork) { + await this.start() + } else if (!ready && this.fork) { + await this.stop() } - - return ctx } - async update(options: Partial, override = false) { + async update(options: Partial, override = false) { const legacy = { ...this.options } // step 1: update options if (override) { - this.options = options as Entry.Options + this.options = options as EntryOptions } else { for (const [key, value] of Object.entries(options)) { if (isNullable(value)) { @@ -286,10 +130,7 @@ export class Entry { if (!this._check()) { await this.stop() } else if (this.fork) { - for (const [key, label] of Object.entries(legacy.isolate ?? {})) { - if (this.options.isolate?.[key] === label || label === true) continue - this.loader.realms[label]?.gc(key) - } + this.parent.ctx.emit('loader/partial-dispose', this, legacy, true) this.patch(options) } else { await this.start() @@ -306,18 +147,11 @@ export class Entry { const ctx = this.patch() ctx[Entry.key] = this this.fork = ctx.plugin(plugin, this.options.config) - ctx.emit('loader/entry', 'apply', this) + ctx.emit('loader/entry-fork', this, 'apply') } async stop() { this.fork?.dispose() this.fork = undefined } - - dispose() { - for (const [key, label] of Object.entries(this.options.isolate ?? {})) { - if (label === true) continue - this.loader.realms[label]?.gc(key) - } - } } diff --git a/packages/loader/src/file.ts b/packages/loader/src/file.ts index f1e9778..cb00df6 100644 --- a/packages/loader/src/file.ts +++ b/packages/loader/src/file.ts @@ -4,7 +4,7 @@ import { access, constants, readdir, readFile, stat, writeFile } from 'node:fs/p import { fileURLToPath, pathToFileURL } from 'node:url' import { remove } from 'cosmokit' import * as yaml from 'js-yaml' -import { Entry } from './entry.ts' +import { EntryOptions } from './entry.ts' import { Loader } from './loader.ts' import { EntryTree } from './tree.ts' import { JsExpr } from './utils.ts' @@ -46,7 +46,7 @@ export class LoaderFile { } } - async read(): Promise { + async read(): Promise { if (this.type === 'application/yaml') { return yaml.load(await readFile(this.name, 'utf8'), { schema }) as any } else if (this.type === 'application/json') { @@ -58,7 +58,7 @@ export class LoaderFile { } } - private async _write(config: Entry.Options[]) { + private async _write(config: EntryOptions[]) { this.suspend = true if (this.readonly) { throw new Error(`cannot overwrite readonly config`) @@ -70,8 +70,7 @@ export class LoaderFile { } } - write(config: Entry.Options[]) { - this.loader.ctx.emit('config') + write(config: EntryOptions[]) { clearTimeout(this._writeTask) this._writeTask = setTimeout(() => { this._writeTask = undefined @@ -123,7 +122,8 @@ export class ImportTree extends EntryTree { } write() { - return this.file!.write(this.root.data) + this.ctx.emit('loader/config-update') + return this.file.write(this.root.data) } _createFile(filename: string, type: string) { diff --git a/packages/loader/src/group.ts b/packages/loader/src/group.ts index 02086d8..d0668d5 100644 --- a/packages/loader/src/group.ts +++ b/packages/loader/src/group.ts @@ -1,18 +1,18 @@ import { Context } from '@cordisjs/core' -import { Entry } from './entry.ts' +import { Entry, EntryOptions } from './entry.ts' import { EntryTree } from './tree.ts' export class EntryGroup { static readonly key = Symbol.for('cordis.group') - public data: Entry.Options[] = [] + public data: EntryOptions[] = [] constructor(public ctx: Context, public tree: EntryTree) { const entry = ctx.scope.entry if (entry) entry.subgroup = this } - async create(options: Omit) { + async create(options: Omit) { const id = this.tree.ensureId(options) const entry = this.tree.store[id] ??= new Entry(this.ctx.loader) // Entry may be moved from another group, @@ -22,7 +22,7 @@ export class EntryGroup { return entry.id } - unlink(options: Entry.Options) { + unlink(options: EntryOptions) { const config = this.data const index = config.indexOf(options) if (index >= 0) config.splice(index, 1) @@ -32,13 +32,13 @@ export class EntryGroup { const entry = this.tree.store[id] if (!entry) return entry.stop() - entry.dispose() this.unlink(entry.options) delete this.tree.store[id] + this.ctx.emit('loader/partial-dispose', entry, entry.options, false) } - update(config: Entry.Options[]) { - const oldConfig = this.data as Entry.Options[] + update(config: EntryOptions[]) { + const oldConfig = this.data as EntryOptions[] this.data = config const oldMap = Object.fromEntries(oldConfig.map(options => [options.id, options])) const newMap = Object.fromEntries(config.map(options => [options.id ?? Symbol('anonymous'), options])) @@ -64,14 +64,14 @@ export class EntryGroup { export class Group extends EntryGroup { static reusable = true - static initial: Omit[] = [] + static initial: Omit[] = [] static readonly [EntryGroup.key] = true // TODO support options constructor(public ctx: Context) { super(ctx, ctx.scope.entry!.parent.tree) ctx.on('dispose', () => this.stop()) - ctx.accept((config: Entry.Options[]) => { + ctx.accept((config: EntryOptions[]) => { this.update(config) }, { passive: true, immediate: true }) } diff --git a/packages/loader/src/inject.ts b/packages/loader/src/inject.ts new file mode 100644 index 0000000..cf2e8ab --- /dev/null +++ b/packages/loader/src/inject.ts @@ -0,0 +1,238 @@ +import { Context, EffectScope, Inject } from '@cordisjs/core' +import { Dict, isNullable } from 'cosmokit' +import { Entry } from './entry.ts' + +declare module './entry.ts' { + interface EntryUpdateMeta { + newMap: Dict + diff: [string, symbol, symbol, symbol, symbol][] + } + + interface EntryOptions { + intercept?: Dict | null + isolate?: Dict | null + inject?: string[] | Inject | null + } + + interface Entry { + realm: LocalRealm + } +} + +function swap(target: T, source?: T | null) { + for (const key of Reflect.ownKeys(target)) { + Reflect.deleteProperty(target, key) + } + for (const key of Reflect.ownKeys(source || {})) { + Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!) + } +} + +export abstract class Realm { + protected store: Dict = Object.create(null) + + abstract get suffix(): string + + access(key: string, create = false) { + if (create) { + return this.store[key] ??= Symbol(`${key}${this.suffix}`) + } else { + return this.store[key] ?? Symbol(`${key}${this.suffix}`) + } + } + + delete(key: string) { + delete this.store[key] + return !Object.keys(this.store).length + } + + get size() { + return Object.keys(this.store).length + } +} + +export class LocalRealm extends Realm { + constructor(private entry: Entry) { + super() + } + + get suffix() { + return '#' + this.entry.options.id + } +} + +export class GlobalRealm extends Realm { + constructor(public label: string) { + super() + } + + get suffix() { + return '@' + this.label + } +} + +export function apply(ctx: Context) { + const realms: Dict = Object.create(null) + + function access(entry: Entry, key: string, create: true): symbol + function access(entry: Entry, key: string, create?: boolean): symbol | undefined + function access(entry: Entry, key: string, create = false) { + let realm: Realm | undefined + const label = entry.options.isolate?.[key] + if (!label) return + if (label === true) { + realm = entry.realm ??= new LocalRealm(entry) + } else if (create) { + realm = realms[label] ??= new GlobalRealm(label) + } else { + realm = realms[label] + } + return realm?.access(key, create) + } + + ctx.on('loader/context-init', (entry, ctx) => { + ctx[Context.intercept] = Object.create(entry.parent.ctx[Context.intercept]) + ctx[Context.isolate] = Object.create(entry.parent.ctx[Context.isolate]) + }) + + ctx.on('loader/before-patch', function (entry, ctx) { + // step 1: generate new isolate map + this.newMap = Object.create(entry.parent.ctx[Context.isolate]) + for (const key of Object.keys(entry.options.isolate ?? {})) { + this.newMap[key] = access(entry, key, true) + } + + // step 2: generate service diff + this.diff = [] + const oldMap = ctx[Context.isolate] + for (const key in { ...this.newMap, ...entry.loader.delims }) { + if (this.newMap[key] === oldMap[key]) continue + const delim = entry.loader.delims[key] ??= Symbol(`delim:${key}`) + ctx[delim] = Symbol(`${key}#${entry.id}`) + for (const symbol of [oldMap[key], this.newMap[key]]) { + const value = symbol && ctx[symbol] + if (!(value instanceof Object)) continue + const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value + if (!source) { + ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`)) + continue + } + this.diff.push([key, oldMap[key], this.newMap[key], ctx[delim], source[delim]]) + if (ctx[delim] !== source[delim]) break + } + } + + // step 3: emit internal/before-service + for (const [key, symbol1, symbol2, flag1, flag2] of this.diff) { + const self = Object.create(ctx) + self[Context.filter] = (target: Context) => { + if (![symbol1, symbol2].includes(target[Context.isolate][key])) return false + return (flag1 === target[entry.loader.delims[key]]) !== (flag1 === flag2) + } + ctx.emit(self, 'internal/before-service', key) + } + + // step 4: set prototype for transferred context + Object.setPrototypeOf(ctx[Context.isolate], entry.parent.ctx[Context.isolate]) + Object.setPrototypeOf(ctx[Context.intercept], entry.parent.ctx[Context.intercept]) + swap(ctx[Context.isolate], this.newMap) + swap(ctx[Context.intercept], entry.options.intercept) + }) + + ctx.on('loader/after-patch', function (entry, ctx) { + // step 5: replace service impl + for (const [, symbol1, symbol2, flag1, flag2] of this.diff) { + if (flag1 === flag2 && ctx[symbol1] && !ctx[symbol2]) { + ctx.root[symbol2] = ctx.root[symbol1] + delete ctx.root[symbol1] + } + } + + // step 6: emit internal/service + for (const [key, symbol1, symbol2, flag1, flag2] of this.diff) { + const self = Object.create(ctx) + self[Context.filter] = (target: Context) => { + if (![symbol1, symbol2].includes(target[Context.isolate][key])) return false + return (flag1 === target[entry.loader.delims[key]]) !== (flag1 === flag2) + } + ctx.emit(self, 'internal/service', key) + } + + // step 7: clean up delimiters + for (const key in entry.loader.delims) { + if (!Reflect.ownKeys(this.newMap).includes(key)) { + delete ctx[entry.loader.delims[key]] + } + } + }) + + ctx.on('loader/partial-dispose', (entry, legacy, active) => { + for (const [key, label] of Object.entries(legacy.isolate ?? {})) { + if (label === true) continue + if (active && entry.options.isolate?.[key] === label) continue + const realm = realms[label] + if (!realm) continue + + // realm garbage collection + for (const entry of ctx.loader.entries()) { + // has reference to this realm + if (entry.options.isolate?.[key] === realm.label) return + } + realm.delete(key) + if (!realm.size) { + delete realms[realm.label] + } + } + }) + + function getRequired(entry?: Entry) { + return Array.isArray(entry?.options.inject) + ? entry.options.inject + : entry?.options.inject?.required ?? [] + } + + function getInject(entry?: Entry) { + return Array.isArray(entry?.options.inject) + ? entry?.options.inject + : [ + ...entry?.options.inject?.required ?? [], + ...entry?.options.inject?.optional ?? [], + ] + } + + const checkInject = (scope: EffectScope, name: string) => { + if (!scope.runtime.plugin) return false + if (scope.runtime === scope) { + return scope.runtime.children.every(fork => checkInject(fork, name)) + } + if (getInject(scope.entry).includes(name)) return true + return checkInject(scope.parent.scope, name) + } + + ctx.on('internal/inject', function (this, name) { + return checkInject(this.scope, name) + }) + + ctx.on('loader/entry-check', (entry) => { + for (const name of getRequired(entry)) { + let key: symbol | undefined = entry.parent.ctx[Context.isolate][name] + const label = entry.options.isolate?.[name] + if (label) key = access(entry, name) + if (!key || isNullable(entry.parent.ctx[key])) return true + } + }) + + ctx.on('internal/before-service', (name) => { + for (const entry of ctx.loader.entries()) { + if (!getRequired(entry).includes(name)) return + entry.refresh() + } + }, { global: true }) + + ctx.on('internal/service', (name) => { + for (const entry of ctx.loader.entries()) { + if (!getRequired(entry).includes(name)) return + entry.refresh() + } + }, { global: true }) +} diff --git a/packages/loader/src/loader.ts b/packages/loader/src/loader.ts index 94e8067..c5ed088 100644 --- a/packages/loader/src/loader.ts +++ b/packages/loader/src/loader.ts @@ -1,8 +1,9 @@ import { Context, EffectScope } from '@cordisjs/core' import { Dict, isNullable } from 'cosmokit' import { ModuleLoader } from './internal.ts' -import { Entry, GlobalRealm } from './entry.ts' +import { Entry, EntryOptions, EntryUpdateMeta } from './entry.ts' import { ImportTree, LoaderFile } from './file.ts' +import * as inject from './inject.ts' export * from './entry.ts' export * from './file.ts' @@ -11,10 +12,14 @@ export * from './tree.ts' declare module '@cordisjs/core' { interface Events { - 'config'(): void 'exit'(signal: NodeJS.Signals): Promise - 'loader/entry'(type: string, entry: Entry): void - 'loader/patch'(entry: Entry, legacy?: Entry.Options): void + 'loader/config-update'(): void + 'loader/entry-fork'(entry: Entry, type: string): void + 'loader/entry-check'(entry: Entry): boolean | undefined + 'loader/partial-dispose'(entry: Entry, legacy: Partial, active: boolean): void + 'loader/context-init'(entry: Entry, ctx: Context): void + 'loader/before-patch'(this: EntryUpdateMeta, entry: Entry, ctx: Context): void + 'loader/after-patch'(this: EntryUpdateMeta, entry: Entry, ctx: Context): void } interface Context { @@ -36,7 +41,7 @@ declare module '@cordisjs/core' { export namespace Loader { export interface Config { name: string - initial?: Omit[] + initial?: Omit[] filename?: string } } @@ -57,21 +62,20 @@ export abstract class Loader extends ImportTree { } public files: Dict = Object.create(null) - public realms: Dict = Object.create(null) public delims: Dict = Object.create(null) public internal?: ModuleLoader constructor(public ctx: Context, public config: Loader.Config) { super(ctx) - this.ctx.set('loader', this) + ctx.set('loader', this) - this.ctx.on('internal/update', (fork) => { + ctx.on('internal/update', (fork) => { if (!fork.entry) return - fork.parent.emit('loader/entry', 'reload', fork.entry) + fork.parent.emit('loader/entry-fork', fork.entry, 'reload') }) - this.ctx.on('internal/before-update', (fork, config) => { + ctx.on('internal/before-update', (fork, config) => { if (!fork.entry) return if (fork.entry.suspend) return fork.entry.suspend = false const { schema } = fork.runtime @@ -79,7 +83,7 @@ export abstract class Loader extends ImportTree { fork.entry.parent.tree.write() }) - this.ctx.on('internal/fork', (fork) => { + ctx.on('internal/fork', (fork) => { // 1. set `fork.entry` if (fork.parent[Entry.key]) { fork.entry = fork.parent[Entry.key] @@ -98,42 +102,20 @@ export abstract class Loader extends ImportTree { // case 3: fork is disposed on behalf of plugin deletion (such as plugin hmr) // self-dispose: ctx.scope.dispose() -> fork / runtime dispose -> delete(plugin) // plugin hmr: delete(plugin) -> runtime dispose -> fork dispose - if (!this.ctx.registry.has(fork.runtime.plugin)) return + if (!ctx.registry.has(fork.runtime.plugin)) return + + fork.entry.fork = undefined + fork.parent.emit('loader/entry-fork', fork.entry, 'unload') // case 4: fork is disposed by loader behavior // such as inject checker, config file update, ancestor group disable if (!fork.entry._check()) return - fork.parent.emit('loader/entry', 'unload', fork.entry) fork.entry.options.disabled = true - fork.entry.fork = undefined fork.entry.parent.tree.write() }) - this.ctx.on('internal/before-service', (name) => { - for (const entry of this.entries()) { - entry.checkService(name) - } - }, { global: true }) - - this.ctx.on('internal/service', (name) => { - for (const entry of this.entries()) { - entry.checkService(name) - } - }, { global: true }) - - const checkInject = (scope: EffectScope, name: string) => { - if (!scope.runtime.plugin) return false - if (scope.runtime === scope) { - return scope.runtime.children.every(fork => checkInject(fork, name)) - } - if (scope.entry?.deps.includes(name)) return true - return checkInject(scope.parent.scope, name) - } - - this.ctx.on('internal/inject', function (this, name) { - return checkInject(this.scope, name) - }) + ctx.plugin(inject) } async start() { diff --git a/packages/loader/src/tree.ts b/packages/loader/src/tree.ts index dc9a6ce..c3cc1d7 100644 --- a/packages/loader/src/tree.ts +++ b/packages/loader/src/tree.ts @@ -1,6 +1,6 @@ import { Context } from '@cordisjs/core' import { Dict } from 'cosmokit' -import { Entry } from './entry.ts' +import { Entry, EntryOptions } from './entry.ts' import { EntryGroup } from './group.ts' export abstract class EntryTree { @@ -25,7 +25,7 @@ export abstract class EntryTree { } } - ensureId(options: Partial) { + ensureId(options: Partial) { if (!options.id) { do { options.id = Math.random().toString(36).slice(2, 8) @@ -54,9 +54,9 @@ export abstract class EntryTree { return entry.subgroup } - async create(options: Omit, parent: string | null = null, position = Infinity) { + async create(options: Omit, parent: string | null = null, position = Infinity) { const group = this.resolveGroup(parent) - group.data.splice(position, 0, options as Entry.Options) + group.data.splice(position, 0, options as EntryOptions) group.tree.write() return group.create(options) } @@ -67,7 +67,7 @@ export abstract class EntryTree { entry.parent.tree.write() } - async update(id: string, options: Omit, parent?: string | null, position?: number) { + async update(id: string, options: Omit, parent?: string | null, position?: number) { const entry = this.resolve(id) const source = entry.parent if (parent !== undefined) { diff --git a/packages/loader/tests/utils.ts b/packages/loader/tests/utils.ts index 793ef98..78904c7 100644 --- a/packages/loader/tests/utils.ts +++ b/packages/loader/tests/utils.ts @@ -1,6 +1,6 @@ import { Dict } from 'cosmokit' import { Context, ForkScope, Plugin } from '@cordisjs/core' -import { LoaderFile, Entry, Group, Loader } from '../src' +import { EntryOptions, Group, Loader, LoaderFile } from '../src' import { Mock, mock } from 'node:test' import { expect } from 'chai' @@ -14,13 +14,13 @@ declare module '../src/index.ts' { } class MockLoaderFile extends LoaderFile { - data: Entry.Options[] = [] + data: EntryOptions[] = [] async read() { return this.data } - write(data: Entry.Options[]) { + write(data: EntryOptions[]) { this.data = data } }