From 94bd84d309242971df5bbf6e38286804a2c94b96 Mon Sep 17 00:00:00 2001 From: Shigma Date: Fri, 24 May 2024 02:56:58 +0800 Subject: [PATCH] feat(loader): use entry group for loader root --- packages/loader/src/entry.ts | 25 +++++---- packages/loader/src/group.ts | 39 +++++++++---- packages/loader/src/shared.ts | 80 ++++++++++++--------------- packages/loader/tests/isolate.spec.ts | 4 +- 4 files changed, 80 insertions(+), 68 deletions(-) diff --git a/packages/loader/src/entry.ts b/packages/loader/src/entry.ts index 8e74651..74d926d 100644 --- a/packages/loader/src/entry.ts +++ b/packages/loader/src/entry.ts @@ -1,6 +1,7 @@ import { Context, ForkScope, Inject } from '@cordisjs/core' import { Dict } from 'cosmokit' import Loader from './shared.ts' +import { EntryGroup } from './group.ts' export namespace Entry { export interface Options { @@ -42,16 +43,17 @@ function sortKeys(object: T, prepend = ['id', 'name'], append = [' } export class Entry { + static key = Symbol('cordis.entry') + public fork?: ForkScope public isUpdate = false - public parent!: Context public options!: Entry.Options - public group: Entry.Options[] | null = null + public children?: EntryGroup - constructor(public loader: Loader) {} + constructor(public loader: Loader, public parent: EntryGroup) {} unlink() { - const config = this.parent.config as Entry.Options[] + const config = this.parent.config const index = config.indexOf(this.options) if (index >= 0) config.splice(index, 1) } @@ -86,7 +88,7 @@ export class Entry { if (!(value instanceof Object)) continue const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value if (!source) { - this.parent.emit('internal/warning', new Error(`expected service ${key} to be implemented`)) + ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`)) continue } diff.push([key, oldMap[key], newMap[key], ctx[delim], source[delim]]) @@ -142,15 +144,14 @@ export class Entry { } createContext() { - return this.parent.extend({ - [Context.intercept]: Object.create(this.parent[Context.intercept]), - [Context.isolate]: Object.create(this.parent[Context.isolate]), + return this.parent.ctx.extend({ + [Context.intercept]: Object.create(this.parent.ctx[Context.intercept]), + [Context.isolate]: Object.create(this.parent.ctx[Context.isolate]), }) } - async update(parent: Context, options: Entry.Options) { + async update(options: Entry.Options) { const legacy = this.options - this.parent = parent this.options = sortKeys(options) if (!this.loader.isTruthyLike(options.when) || options.disabled) { this.stop() @@ -167,9 +168,9 @@ export class Entry { if (!plugin) return const ctx = this.createContext() this.patch(ctx) + ctx[Entry.key] = this this.fork = ctx.plugin(plugin, this.options.config) - this.fork.entry = this - this.parent.emit('loader/entry', 'apply', this) + ctx.emit('loader/entry', 'apply', this) } } diff --git a/packages/loader/src/group.ts b/packages/loader/src/group.ts index fc5f263..3815d70 100644 --- a/packages/loader/src/group.ts +++ b/packages/loader/src/group.ts @@ -1,16 +1,27 @@ import { Context } from '@cordisjs/core' import { Entry } from './entry.ts' -import Loader from './shared.ts' export class EntryGroup { + static inject = ['loader'] + public config: Entry.Options[] = [] - constructor(public loader: Loader, public ctx: Context) { - ctx.on('dispose', () => { - for (const options of this.config) { - this.loader._remove(options.id) - } - }) + constructor(public ctx: Context) {} + + async _create(options: Omit) { + const id = this.ctx.loader.ensureId(options) + const entry = this.ctx.loader.entries[id] ??= new Entry(this.ctx.loader, this) + entry.parent = this + await entry.update(options as Entry.Options) + return id + } + + _remove(id: string) { + const entry = this.ctx.loader.entries[id] + if (!entry) return + entry.stop() + entry.unlink() + delete this.ctx.loader.entries[id] } update(config: Entry.Options[]) { @@ -21,11 +32,19 @@ export class EntryGroup { // update inner plugins for (const id of Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]) { - if (!newMap[id]) { - this.loader._remove(id) + if (newMap[id]) { + this._create(newMap[id]).catch((error) => { + this.ctx.emit('internal/error', error) + }) } else { - this.loader._ensure(this.ctx, newMap[id]) + this._remove(id) } } } + + dispose() { + for (const options of this.config) { + this._remove(options.id) + } + } } diff --git a/packages/loader/src/shared.ts b/packages/loader/src/shared.ts index 6aeeeeb..04268b4 100644 --- a/packages/loader/src/shared.ts +++ b/packages/loader/src/shared.ts @@ -56,6 +56,10 @@ export namespace Loader { } export abstract class Loader extends Service { + static inject = { + optional: ['loader'], + } + // process public baseDir = process.cwd() public envData = process.env.CORDIS_SHARED @@ -66,7 +70,7 @@ export abstract class Loader extends env: process.env, } - public root: Entry + public root: EntryGroup public suspend = false public writable = false public mimeType!: string @@ -83,8 +87,7 @@ export abstract class Loader extends constructor(public app: Context, public options: T) { super(app, 'loader', true) - this.root = new Entry(this) - this.entries[''] = this.root + this.root = new EntryGroup(this.app) this.realms['#'] = app.root[Context.isolate] this.app.on('dispose', () => { @@ -105,6 +108,10 @@ export abstract class Loader extends }) this.app.on('internal/fork', (fork) => { + if (fork.parent[Entry.key]) { + fork.entry = fork.parent[Entry.key] + delete fork.parent[Entry.key] + } // fork.uid: fork is created (we only care about fork dispose event) // fork.parent.runtime.plugin !== group: fork is not tracked by loader if (fork.uid || !fork.entry) return @@ -242,7 +249,7 @@ export abstract class Loader extends return !!this.interpolate(`\${{ ${expr} }}`) } - private ensureId(options: Partial) { + ensureId(options: Partial) { if (!options.id) { do { options.id = Math.random().toString(36).slice(2, 8) @@ -251,13 +258,6 @@ export abstract class Loader extends return options.id! } - async _ensure(parent: Context, options: Omit) { - const id = this.ensureId(options) - const entry = this.entries[id] ??= new Entry(this) - await entry.update(parent, options as Entry.Options) - return id - } - async update(id: string, options: Partial>) { const entry = this.entries[id] if (!entry) throw new Error(`entry ${id} not found`) @@ -270,23 +270,20 @@ export abstract class Loader extends } } this.writeConfig() - return entry.update(entry.parent, override) + return entry.update(override) } - async create(options: Omit, target = '', index = Infinity) { - const targetEntry = this.entries[target] - if (!targetEntry?.fork) throw new Error(`entry ${target} not found`) - targetEntry.options.config.splice(index, 0, options) - this.writeConfig() - return this._ensure(targetEntry.fork.ctx, options) + resolveGroup(id: string | null) { + const group = id ? this.entries[id]?.children : this.root + if (!group) throw new Error(`entry ${id} not found`) + return group } - _remove(id: string) { - const entry = this.entries[id] - if (!entry) return - entry.stop() - entry.unlink() - delete this.entries[id] + async create(options: Omit, parent: string | null = null, position = Infinity) { + const group = this.resolveGroup(parent) + group.config.splice(position, 0, options as Entry.Options) + this.writeConfig() + return group._create(options) } remove(id: string) { @@ -298,17 +295,16 @@ export abstract class Loader extends this.writeConfig() } - transfer(id: string, target: string, index = Infinity) { + transfer(id: string, parent: string | null, position = Infinity) { const entry = this.entries[id] if (!entry) throw new Error(`entry ${id} not found`) - const sourceEntry = entry.parent.scope.entry! - const targetEntry = this.entries[target] - if (!targetEntry?.fork) throw new Error(`entry ${target} not found`) + const source = entry.parent + const target = this.resolveGroup(parent) entry.unlink() - targetEntry.options.config.splice(index, 0, entry.options) + target.config.splice(position, 0, entry.options) this.writeConfig() - if (sourceEntry === targetEntry) return - entry.parent = targetEntry.fork.ctx + if (source === target) return + entry.parent = target if (!entry.fork) return const ctx = entry.createContext() entry.patch(entry.fork.parent, ctx) @@ -331,19 +327,9 @@ export abstract class Loader extends return this._locate(scope.parent.scope) } - createGroup() { - const ctx = this[Context.current] - // if (!ctx.scope.entry) throw new Error(`expected entry scope`) - return new EntryGroup(this, ctx) - } - async start() { await this.readConfig() - this.root.update(this.app, { - id: '', - name: 'cordis/group', - config: this.config, - }) + this.root.update(this.config) while (this.tasks.size) { await Promise.all(this.tasks) @@ -387,13 +373,19 @@ export interface GroupOptions { export function createGroup(config?: Entry.Options[], options: GroupOptions = {}) { options.initial = config - function group(ctx: Context, config: Entry.Options[]) { - const group = ctx.get('loader')!.createGroup() + function group(ctx: Context) { + if (!ctx.scope.entry) throw new Error(`expected entry scope`) + const group = new EntryGroup(ctx) + ctx.scope.entry.children = group + ctx.on('dispose', () => { + group.dispose() + }) ctx.accept((config: Entry.Options[]) => { group.update(config) }, { passive: true, immediate: true }) } + defineProperty(group, 'inject', ['loader']) defineProperty(group, 'reusable', true) defineProperty(group, kGroup, options) if (options.name) defineProperty(group, 'name', options.name) diff --git a/packages/loader/tests/isolate.spec.ts b/packages/loader/tests/isolate.spec.ts index 690e749..7bc4425 100644 --- a/packages/loader/tests/isolate.spec.ts +++ b/packages/loader/tests/isolate.spec.ts @@ -530,7 +530,7 @@ describe('service isolation: transfer', () => { }) it('transfer injector out of group', async () => { - loader.transfer(injector, '') + loader.transfer(injector, null) await new Promise((resolve) => setTimeout(resolve, 0)) expect(foo.mock.calls).to.have.length(0) @@ -538,7 +538,7 @@ describe('service isolation: transfer', () => { }) it('transfer provider out of group', async () => { - loader.transfer(provider, '') + loader.transfer(provider, null) await new Promise((resolve) => setTimeout(resolve, 0)) expect(foo.mock.calls).to.have.length(1)