diff --git a/packages/cordis/src/index.ts b/packages/cordis/src/index.ts index 99a99e9..b2d4ddc 100644 --- a/packages/cordis/src/index.ts +++ b/packages/cordis/src/index.ts @@ -22,11 +22,9 @@ export class Context extends core.Context { this.baseDir = globalThis.process?.cwd() || '' this.provide('logger', undefined, true) - this.provide('schema', undefined, true) this.provide('timer', undefined, true) this.plugin(LoggerService) - this.plugin(SchemaService) this.plugin(TimerService) } } @@ -34,6 +32,7 @@ export class Context extends core.Context { export abstract class Service extends core.Service { /** @deprecated use `this.ctx.logger` instead */ public logger: Logger + public schema: SchemaService constructor(...args: core.Spread) constructor(ctx: C, ...args: core.Spread) @@ -41,6 +40,7 @@ export abstract class Service extends constructor(...args: any) { super(...args) this.logger = this.ctx.logger(this.name) + this.schema = new SchemaService(this.ctx) } [core.Service.setup]() { diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index 602db7d..35bf82e 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -82,7 +82,7 @@ export default class Lifecycle { // non-reusable plugin forks are not responsive to isolated service changes defineProperty(this.on('internal/before-service', function (this: Context, name) { for (const runtime of this.registry.values()) { - if (!runtime.using.includes(name)) continue + if (!runtime.inject[name]?.required) continue const scopes = runtime.isReusable ? runtime.children : [runtime] for (const scope of scopes) { if (!this[symbols.filter](scope.ctx)) continue @@ -94,7 +94,7 @@ export default class Lifecycle { defineProperty(this.on('internal/service', function (this: Context, name) { for (const runtime of this.registry.values()) { - if (!runtime.using.includes(name)) continue + if (!runtime.inject[name]?.required) continue const scopes = runtime.isReusable ? runtime.children : [runtime] for (const scope of scopes) { if (!this[symbols.filter](scope.ctx)) continue @@ -106,7 +106,7 @@ export default class Lifecycle { // inject in ancestor contexts const checkInject = (scope: EffectScope, name: string) => { if (!scope.runtime.plugin) return false - for (const key of scope.runtime.inject) { + for (const key in scope.runtime.inject) { if (name === ReflectService.resolveInject(scope.ctx, key)[0]) return true } return checkInject(scope.parent.scope, name) diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index dff1c90..c167555 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -3,13 +3,31 @@ import { Context } from './context.ts' import { ForkScope, MainScope } from './scope.ts' import { resolveConfig, symbols } from './utils.ts' -export function isApplicable(object: Plugin) { +function isApplicable(object: Plugin) { return object && typeof object === 'object' && typeof object.apply === 'function' } -export interface Inject { - readonly required?: string[] - readonly optional?: string[] +export type Inject = string[] | Dict + +export namespace Inject { + export interface Meta { + required: boolean + } + + export function resolve(inject: Inject | null | undefined) { + if (!inject) return {} + if (Array.isArray(inject)) { + return Object.fromEntries(inject.map(name => [name, { required: true }])) + } + const { required, optional, ...rest } = inject + if (Array.isArray(required)) { + Object.assign(rest, Object.fromEntries(required.map(name => [name, { required: true }]))) + } + if (Array.isArray(optional)) { + Object.assign(rest, Object.fromEntries(optional.map(name => [name, { required: false }]))) + } + return rest + } } export type Plugin = @@ -23,7 +41,7 @@ export namespace Plugin { reactive?: boolean reusable?: boolean Config?: (config: any) => T - inject?: string[] | Inject + inject?: Inject intercept?: Dict } @@ -50,8 +68,8 @@ export type Spread = undefined extends T ? [config?: T] : [config: T] declare module './context.ts' { export interface Context { /** @deprecated use `ctx.inject()` instead */ - using(deps: string[] | Inject, callback: Plugin.Function): ForkScope - inject(deps: string[] | Inject, callback: Plugin.Function): ForkScope + using(deps: Inject, callback: Plugin.Function): ForkScope + inject(deps: Inject, callback: Plugin.Function): ForkScope plugin(plugin: Plugin.Function & Plugin.Transform, ...args: Spread): ForkScope plugin(plugin: Plugin.Constructor & Plugin.Transform, ...args: Spread): ForkScope plugin(plugin: Plugin.Object & Plugin.Transform, ...args: Spread): ForkScope @@ -135,11 +153,11 @@ export default class Registry { return this._internal.forEach(callback) } - using(inject: string[] | Inject, callback: Plugin.Function) { + using(inject: Inject, callback: Plugin.Function) { return this.inject(inject, callback) } - inject(inject: string[] | Inject, callback: Plugin.Function) { + inject(inject: Inject, callback: Plugin.Function) { return this.plugin({ inject, apply: callback, name: callback.name }) } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 50dfd31..88817ab 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -1,4 +1,4 @@ -import { deepEqual, defineProperty, isNullable, remove } from 'cosmokit' +import { deepEqual, defineProperty, Dict, isNullable, remove } from 'cosmokit' import { Context } from './context.ts' import { Inject, Plugin } from './registry.ts' import { isConstructor, resolveConfig } from './utils.ts' @@ -167,7 +167,9 @@ export abstract class EffectScope { } get ready() { - return this.runtime.using.every(name => !isNullable(this.ctx.get(name))) + return Object.entries(this.runtime.inject).every(([name, inject]) => { + return !inject.required || !isNullable(this.ctx.get(name)) + }) } reset() { @@ -303,8 +305,7 @@ export class MainScope extends EffectScope { runtime = this schema: any name?: string - using: string[] = [] - inject = new Set() + inject: Dict = Object.create(null) forkables: Function[] = [] children: ForkScope[] = [] isReusable?: boolean = false @@ -336,28 +337,11 @@ export class MainScope extends EffectScope { return true } - private setInject(inject?: string[] | Inject): void { - if (Array.isArray(inject)) { - for (const name of inject) { - this.using.push(name) - this.inject.add(name) - } - } else if (inject) { - for (const name of inject.required || []) { - this.using.push(name) - this.inject.add(name) - } - for (const name of inject.optional || []) { - this.inject.add(name) - } - } - } - private setup() { const { name } = this.plugin if (name && name !== 'apply') this.name = name this.schema = this.plugin['Config'] || this.plugin['schema'] - this.setInject(this.plugin['using'] || this.plugin['inject']) + this.inject = Inject.resolve(this.plugin['using'] || this.plugin['inject']) this.isReusable = this.plugin['reusable'] this.isReactive = this.plugin['reactive'] this.context.emit('internal/runtime', this) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 9b4d947..e9d802e 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -10,6 +10,7 @@ export const symbols = { // internal symbols shadow: Symbol.for('cordis.shadow'), receiver: Symbol.for('cordis.receiver'), + original: Symbol.for('cordis.original'), // context symbols source: Symbol.for('cordis.source') as typeof Context.source, @@ -133,6 +134,7 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo } const proxy = new Proxy(value, { get: (target, prop, receiver) => { + if (prop === symbols.original) return target if (prop === tracker.property) return ctx if (typeof prop === 'symbol') { return Reflect.get(target, prop, receiver) @@ -152,6 +154,7 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo } }, set: (target, prop, value, receiver) => { + if (prop === symbols.original) return false if (prop === tracker.property) return false if (typeof prop === 'symbol') { return Reflect.set(target, prop, value, receiver) diff --git a/packages/loader/src/inject.ts b/packages/loader/src/inject.ts index 925cace..84001fa 100644 --- a/packages/loader/src/inject.ts +++ b/packages/loader/src/inject.ts @@ -1,28 +1,18 @@ import { Context, EffectScope, Inject } from '@cordisjs/core' +import { filterKeys } from 'cosmokit' import { Entry } from './entry.ts' declare module './entry.ts' { interface EntryOptions { - inject?: string[] | Inject | null + inject?: Inject | null } } export const name = 'inject' export function apply(ctx: Context) { - 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 ?? [], - ] + function getRequired(entry: Entry) { + return filterKeys(Inject.resolve(entry.options.inject), (_, meta) => meta.required) } const checkInject = (scope: EffectScope, name: string) => { @@ -30,7 +20,7 @@ export function apply(ctx: Context) { if (scope.runtime === scope) { return scope.runtime.children.every(fork => checkInject(fork, name)) } - if (getInject(scope.entry).includes(name)) return true + if (name in Inject.resolve(scope.entry?.options.inject)) return true return checkInject(scope.parent.scope, name) } @@ -39,21 +29,21 @@ export function apply(ctx: Context) { }) ctx.on('loader/entry-check', (entry) => { - for (const name of getRequired(entry)) { + for (const name in getRequired(entry)) { if (!entry.ctx.get(name)) return true } }) ctx.on('internal/before-service', (name) => { for (const entry of ctx.loader.entries()) { - if (!getRequired(entry).includes(name)) continue + if (!(name in getRequired(entry))) continue entry.refresh() } }, { global: true }) ctx.on('internal/service', (name) => { for (const entry of ctx.loader.entries()) { - if (!getRequired(entry).includes(name)) continue + if (!(name in getRequired(entry))) continue entry.refresh() } }, { global: true }) diff --git a/packages/loader/src/loader.ts b/packages/loader/src/loader.ts index 32242b0..82210bc 100644 --- a/packages/loader/src/loader.ts +++ b/packages/loader/src/loader.ts @@ -50,11 +50,9 @@ export namespace Loader { export abstract class Loader extends ImportTree { // TODO auto inject optional when provided? static inject = { - optional: ['loader'], + loader: { required: false }, } - private [Context.current]!: C - // process public envData = process.env.CORDIS_SHARED ? JSON.parse(process.env.CORDIS_SHARED) @@ -127,7 +125,7 @@ export abstract class Loader extends ImportTree await super.start() } - locate(ctx = this[Context.current]) { + locate(ctx = this.ctx) { return this._locate(ctx.scope).map(entry => entry.id) } diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index b328150..8d7c992 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,4 +1,4 @@ -import { Dict, remove } from 'cosmokit' +import { defineProperty, remove } from 'cosmokit' import { Context, Service } from '@cordisjs/core' import Schema from 'schemastery' @@ -7,51 +7,39 @@ export { default as Schema, default as z } from 'schemastery' const kSchemaOrder = Symbol('cordis.schema.order') declare module '@cordisjs/core' { - interface Context { - schema: SchemaService - } - interface Events { - 'internal/schema'(name: string): void + 'internal/service-schema'(): void } } -export class SchemaService extends Service { - _data: Dict = Object.create(null) +export class SchemaService { + _data = Schema.intersect([]) as Schema & { list: Schema[] } constructor(public ctx: Context) { - super(ctx, 'schema', true) + defineProperty(this, Service.tracker, { + property: 'ctx', + }) } - extend(name: string, schema: Schema, order = 0) { - const caller = this[Context.current] - const target = this.get(name) - const index = target.list.findIndex(a => a[kSchemaOrder] < order) + extend(schema: Schema, order = 0) { + const index = this._data.list.findIndex(a => a[kSchemaOrder] < order) schema[kSchemaOrder] = order - if (index >= 0) { - target.list.splice(index, 0, schema) - } else { - target.list.push(schema) - } - this.ctx.emit('internal/schema', name) - caller.on('dispose', () => { - remove(target.list, schema) - this.ctx.emit('internal/schema', name) + return this.ctx.effect(() => { + if (index >= 0) { + this._data.list.splice(index, 0, schema) + } else { + this._data.list.push(schema) + } + this.ctx.emit('internal/service-schema') + return () => { + remove(this._data.list, schema) + this.ctx.emit('internal/service-schema') + } }) } - get(name: string) { - return (this._data[name] ||= Schema.intersect([])) as Schema & { list: Schema[] } - } - - set(name: string, schema: Schema) { - const caller = this[Context.current] - this._data[name] = schema - this.ctx.emit('internal/schema', name) - caller?.on('dispose', () => { - delete this._data[name] - this.ctx.emit('internal/schema', name) - }) + toJSON() { + return this._data.toJSON() } }